diff --git a/fluss-codegen/pom.xml b/fluss-codegen/pom.xml new file mode 100644 index 0000000000..b8ead0b991 --- /dev/null +++ b/fluss-codegen/pom.xml @@ -0,0 +1,124 @@ + + + + + 4.0.0 + + org.apache.fluss + fluss + 0.9-SNAPSHOT + + + fluss-codegen + + Fluss : Code Gen + jar + + Code generation module for Fluss, providing runtime code generation + for high-performance record comparison and projection operations. + + + + 3.1.9 + + + + + org.apache.fluss + fluss-common + ${project.version} + + + + + org.codehaus.janino + janino + ${janino.version} + + + + org.codehaus.janino + commons-compiler + ${janino.version} + + + + + org.apache.fluss + fluss-test-utils + + + + org.apache.fluss + fluss-common + ${project.version} + test-jar + test + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + shade-janino + package + + shade + + + + + org.codehaus.janino:janino + org.codehaus.janino:commons-compiler + + + + + org.codehaus.janino + org.apache.fluss.shaded.org.codehaus.janino + + + org.codehaus.commons + org.apache.fluss.shaded.org.codehaus.commons + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/CodeGenException.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/CodeGenException.java new file mode 100644 index 0000000000..4f7a92ae8d --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/CodeGenException.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.apache.fluss.annotation.Internal; +import org.apache.fluss.exception.FlussRuntimeException; + +/** + * Exception for all errors occurring during code generation. + * + *

This exception is thrown when: + * + *

+ */ +@Internal +public class CodeGenException extends FlussRuntimeException { + + private static final long serialVersionUID = 1L; + + public CodeGenException(String message) { + super(message); + } + + public CodeGenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/CodeGeneratorContext.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/CodeGeneratorContext.java new file mode 100644 index 0000000000..d95d7ac287 --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/CodeGeneratorContext.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.apache.fluss.utils.InstantiationUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * The context for code generator, maintaining various reusable statements that could be inserted + * into different code sections in the final generated class. + */ +public class CodeGeneratorContext { + + private static final AtomicLong NAME_COUNTER = new AtomicLong(0); + + /** Holding a list of objects that could be passed into generated class. */ + private final List references = new ArrayList<>(); + + /** Set of member statements that will be added only once. */ + private final LinkedHashSet reusableMemberStatements = new LinkedHashSet<>(); + + /** Set of constructor statements that will be added only once. */ + private final LinkedHashSet reusableInitStatements = new LinkedHashSet<>(); + + public CodeGeneratorContext() {} + + /** + * Adds a reusable member field statement to the member area. + * + * @param memberStatement the member field declare statement + */ + public void addReusableMember(String memberStatement) { + reusableMemberStatements.add(memberStatement); + } + + /** + * Adds a reusable Object to the member area of the generated class. The object must be + * Serializable. + * + * @param obj the object to be added to the generated class (must be Serializable) + * @param fieldNamePrefix prefix field name of the generated member field term + * @param fieldTypeTerm field type class name + * @return the generated unique field term + */ + public String addReusableObject( + T obj, String fieldNamePrefix, String fieldTypeTerm) { + String fieldTerm = newName(fieldNamePrefix); + addReusableObjectInternal(obj, fieldTerm, fieldTypeTerm); + return fieldTerm; + } + + private void addReusableObjectInternal( + T obj, String fieldTerm, String fieldTypeTerm) { + int idx = references.size(); + // make a deep copy of the object + try { + Object objCopy = InstantiationUtils.clone(obj); + references.add(objCopy); + } catch (Exception e) { + throw new CodeGenException("Failed to clone object: " + obj, e); + } + + reusableMemberStatements.add("private transient " + fieldTypeTerm + " " + fieldTerm + ";"); + reusableInitStatements.add( + fieldTerm + " = ((" + fieldTypeTerm + ") references[" + idx + "]);"); + } + + /** Adds a reusable init statement which will be placed in constructor. */ + public void addReusableInitStatement(String statement) { + reusableInitStatements.add(statement); + } + + /** + * @return code block of statements that need to be placed in the member area of the class + */ + public String reuseMemberCode() { + return String.join("\n", reusableMemberStatements); + } + + /** + * @return code block of statements that need to be placed in the constructor + */ + public String reuseInitCode() { + return String.join("\n", reusableInitStatements); + } + + /** + * @return the list of reference objects + */ + public Object[] getReferences() { + return references.toArray(); + } + + /** + * Generates a new unique name with the given prefix. + * + * @param name the name prefix + * @return a unique name + */ + public static String newName(String name) { + return name + "$" + NAME_COUNTER.getAndIncrement(); + } +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/CompileUtils.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/CompileUtils.java new file mode 100644 index 0000000000..7035f16b9b --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/CompileUtils.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.apache.fluss.utils.MapUtils; + +import org.codehaus.janino.SimpleCompiler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Objects; + +import static org.apache.fluss.utils.Preconditions.checkNotNull; + +/** Utilities to compile a generated code to a Class. */ +public final class CompileUtils { + + private static final Logger LOG = LoggerFactory.getLogger(CompileUtils.class); + + /** + * Cache of compiled classes. Janino generates a new Class Loader and a new Class file every + * compile (guaranteeing that the class name will not be repeated). This leads to multiple tasks + * of the same process that generate a large number of duplicate class, resulting in a large + * number of Meta zone GC (class unloading), resulting in performance bottlenecks. So we add a + * cache to avoid this problem. + */ + private static final Map> COMPILED_CLASS_CACHE = + MapUtils.newConcurrentHashMap(); + + private CompileUtils() {} + + /** + * Compiles a generated code to a Class. + * + * @param classLoader the ClassLoader used to load the class + * @param name the class name + * @param code the generated code + * @param the class type + * @return the compiled class + */ + @SuppressWarnings("unchecked") + public static Class compile(ClassLoader classLoader, String name, String code) { + checkNotNull(classLoader, "classLoader must not be null"); + checkNotNull(name, "name must not be null"); + checkNotNull(code, "code must not be null"); + + // The class name is part of the "code" and makes the string unique, + // to prevent class leaks we don't cache the class loader directly + // but only its hash code + ClassKey classKey = new ClassKey(classLoader.hashCode(), code); + return (Class) + COMPILED_CLASS_CACHE.computeIfAbsent( + classKey, key -> doCompile(classLoader, name, code)); + } + + private static Class doCompile(ClassLoader classLoader, String name, String code) { + LOG.debug("Compiling: {} \n\n Code:\n{}", name, code); + SimpleCompiler compiler = new SimpleCompiler(); + compiler.setParentClassLoader(classLoader); + try { + compiler.cook(code); + } catch (Throwable t) { + LOG.error("Failed to compile code:\n{}", addLineNumber(code)); + throw new CodeGenException( + "Code generation cannot be compiled. This is a bug. Please file an issue.", t); + } + try { + return compiler.getClassLoader().loadClass(name); + } catch (ClassNotFoundException e) { + throw new CodeGenException("Cannot load class " + name, e); + } + } + + /** + * To output more information when an error occurs. Generally, when cook fails, it shows which + * line is wrong. This line number starts at 1. + */ + private static String addLineNumber(String code) { + String[] lines = code.split("\n"); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + builder.append("/* ").append(i + 1).append(" */").append(lines[i]).append("\n"); + } + return builder.toString(); + } + + /** Clear the compiled class cache. Mainly for testing purposes. */ + public static void clearCache() { + COMPILED_CLASS_CACHE.clear(); + } + + /** Class to use as key for the compiled class cache. */ + private static final class ClassKey { + private final int classLoaderId; + private final String code; + + private ClassKey(int classLoaderId, String code) { + this.classLoaderId = classLoaderId; + this.code = code; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClassKey classKey = (ClassKey) o; + return classLoaderId == classKey.classLoaderId && code.equals(classKey.code); + } + + @Override + public int hashCode() { + return Objects.hash(classLoaderId, code); + } + } +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/GeneratedClass.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/GeneratedClass.java new file mode 100644 index 0000000000..75c34dd4b7 --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/GeneratedClass.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import java.io.Serializable; + +import static org.apache.fluss.utils.Preconditions.checkNotNull; + +/** + * A wrapper for generated class, defines a {@link #newInstance(ClassLoader)} method to get an + * instance by reference objects easily. + * + * @param the type of the generated class + */ +public final class GeneratedClass implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String className; + private final String code; + private final Object[] references; + + private transient Class compiledClass; + + public GeneratedClass(String className, String code) { + this(className, code, new Object[0]); + } + + public GeneratedClass(String className, String code, Object[] references) { + this.className = checkNotNull(className, "className must not be null"); + this.code = checkNotNull(code, "code must not be null"); + this.references = checkNotNull(references, "references must not be null"); + } + + /** + * Create a new instance of this generated class. + * + * @param classLoader the class loader to use for loading the compiled class + * @return a new instance of the generated class + */ + public T newInstance(ClassLoader classLoader) { + try { + return compile(classLoader) + .getConstructor(Object[].class) + // Because Constructor.newInstance(Object... initargs), we need to load + // references into a new Object[], otherwise it cannot be compiled. + .newInstance(new Object[] {references}); + } catch (Throwable e) { + throw new RuntimeException( + "Could not instantiate generated class '" + className + "'", e); + } + } + + /** + * Compiles the generated code, the compiled class will be cached in the {@link GeneratedClass}. + * + * @param classLoader the class loader to use for compilation + * @return the compiled class + */ + @SuppressWarnings("unchecked") + public Class compile(ClassLoader classLoader) { + if (compiledClass == null) { + compiledClass = (Class) CompileUtils.compile(classLoader, className, code); + } + return compiledClass; + } + + public String getClassName() { + return className; + } + + public String getCode() { + return code; + } + + public Object[] getReferences() { + return references; + } +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/JavaCodeBuilder.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/JavaCodeBuilder.java new file mode 100644 index 0000000000..5a0e45bbdf --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/JavaCodeBuilder.java @@ -0,0 +1,774 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * A fluent builder for generating Java source code with proper indentation. + * + *

This builder provides a type-safe, readable API for constructing Java code, handling + * indentation automatically and supporting common code patterns like classes, methods, if-else + * blocks, and loops. + * + *

Features: + * + *

    + *
  • Type-safe modifiers via {@link Modifier} enum + *
  • Type-safe primitive types via {@link PrimitiveType} enum + *
  • Convenient type references via {@link #typeOf(Class)} + *
  • Automatic indentation management + *
+ * + *

Example usage: + * + *

{@code
+ * import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.*;
+ * import static org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType.*;
+ * import static org.apache.fluss.codegen.JavaCodeBuilder.Param.of;
+ *
+ * String code = new JavaCodeBuilder()
+ *     .beginClass(new Modifier[]{PUBLIC, FINAL}, "MyClass", typeOf(Serializable.class))
+ *         .field(new Modifier[]{PRIVATE}, INT, "count")
+ *         .newLine()
+ *         .beginConstructor(PUBLIC, "MyClass", of(arrayOf(Object.class), "references"))
+ *             .stmt("this.count = 0")
+ *         .endConstructor()
+ *         .newLine()
+ *         .beginMethod(PUBLIC, BOOLEAN, "equals", of("InternalRow", "left"), of("InternalRow", "right"))
+ *             .beginIf("left == null")
+ *                 .returnStmt("false")
+ *             .endIf()
+ *             .returnStmt("left.equals(right)")
+ *         .endMethod()
+ *     .endClass()
+ *     .build();
+ * }
+ */ +public class JavaCodeBuilder { + + private static final String INDENT = " "; + + private final StringBuilder code; + private int indentLevel; + + public JavaCodeBuilder() { + this.code = new StringBuilder(); + this.indentLevel = 0; + } + + // ==================== Type-Safe Enums ==================== + + /** Java access and non-access modifiers. */ + public enum Modifier { + PUBLIC("public"), + PRIVATE("private"), + PROTECTED("protected"), + STATIC("static"), + FINAL("final"), + ABSTRACT("abstract"), + TRANSIENT("transient"), + VOLATILE("volatile"), + SYNCHRONIZED("synchronized"), + NATIVE("native"); + + private final String keyword; + + Modifier(String keyword) { + this.keyword = keyword; + } + + @Override + public String toString() { + return keyword; + } + } + + /** Java primitive types. */ + public enum PrimitiveType { + BOOLEAN("boolean"), + BYTE("byte"), + CHAR("char"), + SHORT("short"), + INT("int"), + LONG("long"), + FLOAT("float"), + DOUBLE("double"), + VOID("void"); + + private final String keyword; + + PrimitiveType(String keyword) { + this.keyword = keyword; + } + + @Override + public String toString() { + return keyword; + } + } + + // ==================== Parameter Class ==================== + + /** + * Represents a method or constructor parameter with type and name. + * + *

Example usage: + * + *

{@code
+     * // Using primitive type
+     * Param.of(INT, "count")
+     *
+     * // Using class type
+     * Param.of(String.class, "name")
+     *
+     * // Using type string
+     * Param.of("InternalRow", "row")
+     * }
+ */ + public static final class Param { + private final String type; + private final String name; + + private Param(String type, String name) { + this.type = type; + this.name = name; + } + + /** Creates a parameter with a primitive type. */ + public static Param of(PrimitiveType type, String name) { + return new Param(type.toString(), name); + } + + /** Creates a parameter with a class type. */ + public static Param of(Class type, String name) { + return new Param(type.getCanonicalName(), name); + } + + /** Creates a parameter with a type string. */ + public static Param of(String type, String name) { + return new Param(type, name); + } + + /** Returns the type of this parameter. */ + public String getType() { + return type; + } + + /** Returns the name of this parameter. */ + public String getName() { + return name; + } + + @Override + public String toString() { + return type + " " + name; + } + } + + // ==================== Static Helper Methods ==================== + + /** + * Combines multiple modifiers into a space-separated string. + * + * @param modifiers the modifiers to combine + * @return the combined modifier string + */ + public static String mods(Modifier... modifiers) { + return Arrays.stream(modifiers).map(Modifier::toString).collect(Collectors.joining(" ")); + } + + /** + * Combines multiple parameters into a comma-separated string. + * + * @param params the parameters to combine + * @return the combined parameter string + */ + public static String params(Param... params) { + return Arrays.stream(params).map(Param::toString).collect(Collectors.joining(", ")); + } + + /** + * Returns the canonical name of a class for use in generated code. + * + * @param clazz the class + * @return the canonical class name + */ + public static String typeOf(Class clazz) { + return clazz.getCanonicalName(); + } + + /** + * Returns the array type string for a given element type. + * + * @param elementType the element type + * @return the array type string (e.g., "int[]") + */ + public static String arrayOf(PrimitiveType elementType) { + return elementType.toString() + "[]"; + } + + /** + * Returns the array type string for a given class. + * + * @param clazz the element class + * @return the array type string (e.g., "String[]") + */ + public static String arrayOf(Class clazz) { + return clazz.getCanonicalName() + "[]"; + } + + /** + * Returns the array type string for a given type name. + * + * @param typeName the element type name + * @return the array type string + */ + public static String arrayOf(String typeName) { + return typeName + "[]"; + } + + // ==================== Class Structure ==================== + + /** + * Begins a class declaration with type-safe modifiers. + * + * @param modifiers class modifiers + * @param className the class name + * @param implementsInterface the interface to implement (can be null) + * @return this builder for chaining + */ + public JavaCodeBuilder beginClass( + Modifier[] modifiers, String className, String implementsInterface) { + return beginClassInternal(mods(modifiers), className, implementsInterface); + } + + /** + * Begins a class declaration with a single modifier. + * + * @param modifier class modifier + * @param className the class name + * @param implementsInterface the interface to implement (can be null) + * @return this builder for chaining + */ + public JavaCodeBuilder beginClass( + Modifier modifier, String className, String implementsInterface) { + return beginClassInternal(modifier.toString(), className, implementsInterface); + } + + /** Internal implementation for beginClass. */ + private JavaCodeBuilder beginClassInternal( + String modifiers, String className, String implementsInterface) { + indent(); + code.append(modifiers).append(" class ").append(className); + if (implementsInterface != null && !implementsInterface.isEmpty()) { + code.append(" implements ").append(implementsInterface); + } + code.append(" {\n"); + indentLevel++; + return this; + } + + /** Ends a class declaration. */ + public JavaCodeBuilder endClass() { + indentLevel--; + indent(); + code.append("}\n"); + return this; + } + + // ==================== Fields ==================== + + /** + * Adds a field declaration with type-safe modifiers and primitive type. + * + * @param modifiers field modifiers + * @param type the primitive type + * @param name the field name + * @return this builder for chaining + */ + public JavaCodeBuilder field(Modifier[] modifiers, PrimitiveType type, String name) { + return fieldInternal(mods(modifiers), type.toString(), name); + } + + /** + * Adds a field declaration with type-safe modifiers. + * + * @param modifiers field modifiers + * @param type the field type + * @param name the field name + * @return this builder for chaining + */ + public JavaCodeBuilder field(Modifier[] modifiers, String type, String name) { + return fieldInternal(mods(modifiers), type, name); + } + + /** + * Adds a field declaration with single modifier and primitive type. + * + * @param modifier field modifier + * @param type the primitive type + * @param name the field name + * @return this builder for chaining + */ + public JavaCodeBuilder field(Modifier modifier, PrimitiveType type, String name) { + return fieldInternal(modifier.toString(), type.toString(), name); + } + + /** + * Adds a field declaration with single modifier. + * + * @param modifier field modifier + * @param type the field type + * @param name the field name + * @return this builder for chaining + */ + public JavaCodeBuilder field(Modifier modifier, String type, String name) { + return fieldInternal(modifier.toString(), type, name); + } + + /** Internal implementation for field. */ + private JavaCodeBuilder fieldInternal(String modifiers, String type, String name) { + indent(); + code.append(modifiers).append(" ").append(type).append(" ").append(name).append(";\n"); + return this; + } + + /** + * Adds a field declaration with initialization. + * + * @param modifiers field modifiers + * @param type the field type + * @param name the field name + * @param initialValue the initial value expression + * @return this builder for chaining + */ + public JavaCodeBuilder fieldWithInit( + Modifier[] modifiers, PrimitiveType type, String name, String initialValue) { + return fieldWithInitInternal(mods(modifiers), type.toString(), name, initialValue); + } + + /** + * Adds a field declaration with initialization. + * + * @param modifiers field modifiers + * @param type the field type + * @param name the field name + * @param initialValue the initial value expression + * @return this builder for chaining + */ + public JavaCodeBuilder fieldWithInit( + Modifier[] modifiers, String type, String name, String initialValue) { + return fieldWithInitInternal(mods(modifiers), type, name, initialValue); + } + + /** + * Adds a field declaration with initialization using single modifier. + * + * @param modifier field modifier + * @param type the field type + * @param name the field name + * @param initialValue the initial value expression + * @return this builder for chaining + */ + public JavaCodeBuilder fieldWithInit( + Modifier modifier, String type, String name, String initialValue) { + return fieldWithInitInternal(modifier.toString(), type, name, initialValue); + } + + /** + * Adds a field declaration with initialization using single modifier and primitive type. + * + * @param modifier field modifier + * @param type the primitive type + * @param name the field name + * @param initialValue the initial value expression + * @return this builder for chaining + */ + public JavaCodeBuilder fieldWithInit( + Modifier modifier, PrimitiveType type, String name, String initialValue) { + return fieldWithInitInternal(modifier.toString(), type.toString(), name, initialValue); + } + + /** Internal implementation for fieldWithInit. */ + private JavaCodeBuilder fieldWithInitInternal( + String modifiers, String type, String name, String initialValue) { + indent(); + code.append(modifiers).append(" ").append(type).append(" ").append(name); + code.append(" = ").append(initialValue).append(";\n"); + return this; + } + + // ==================== Constructor ==================== + + /** + * Begins a constructor declaration with type-safe modifier and parameters. + * + * @param modifier constructor modifier + * @param className the class name + * @param params the type-safe parameters + * @return this builder for chaining + */ + public JavaCodeBuilder beginConstructor(Modifier modifier, String className, Param... params) { + return beginConstructorInternal(modifier.toString(), className, params(params)); + } + + /** Internal implementation for beginConstructor. */ + private JavaCodeBuilder beginConstructorInternal( + String modifiers, String className, String params) { + indent(); + code.append(modifiers).append(" ").append(className).append("(").append(params).append(")"); + code.append(" throws Exception {\n"); + indentLevel++; + return this; + } + + /** Ends a constructor. */ + public JavaCodeBuilder endConstructor() { + indentLevel--; + indent(); + code.append("}\n"); + return this; + } + + // ==================== Methods ==================== + + /** + * Begins a method declaration with type-safe modifier, primitive return type, and parameters. + * + * @param modifier method modifier + * @param returnType the primitive return type + * @param methodName the method name + * @param params the type-safe parameters + * @return this builder for chaining + */ + public JavaCodeBuilder beginMethod( + Modifier modifier, PrimitiveType returnType, String methodName, Param... params) { + return beginMethodInternal( + modifier.toString(), returnType.toString(), methodName, params(params)); + } + + /** + * Begins a method declaration with type-safe modifier and parameters. + * + * @param modifier method modifier + * @param returnType the return type + * @param methodName the method name + * @param params the type-safe parameters + * @return this builder for chaining + */ + public JavaCodeBuilder beginMethod( + Modifier modifier, String returnType, String methodName, Param... params) { + return beginMethodInternal(modifier.toString(), returnType, methodName, params(params)); + } + + /** Internal implementation for beginMethod. */ + private JavaCodeBuilder beginMethodInternal( + String modifiers, String returnType, String methodName, String params) { + indent(); + code.append(modifiers).append(" ").append(returnType).append(" ").append(methodName); + code.append("(").append(params).append(") {\n"); + indentLevel++; + return this; + } + + /** Adds an @Override annotation. */ + public JavaCodeBuilder override() { + indent(); + code.append("@Override\n"); + return this; + } + + /** Ends a method. */ + public JavaCodeBuilder endMethod() { + indentLevel--; + indent(); + code.append("}\n"); + return this; + } + + // ==================== Control Flow ==================== + + /** + * Begins an if block. + * + * @param condition the condition expression + * @return this builder for chaining + */ + public JavaCodeBuilder beginIf(String condition) { + indent(); + code.append("if (").append(condition).append(") {\n"); + indentLevel++; + return this; + } + + /** Ends an if block. */ + public JavaCodeBuilder endIf() { + indentLevel--; + indent(); + code.append("}\n"); + return this; + } + + /** + * Begins an else-if block. + * + * @param condition the condition expression + * @return this builder for chaining + */ + public JavaCodeBuilder beginElseIf(String condition) { + indentLevel--; + indent(); + code.append("} else if (").append(condition).append(") {\n"); + indentLevel++; + return this; + } + + /** Begins an else block. */ + public JavaCodeBuilder beginElse() { + indentLevel--; + indent(); + code.append("} else {\n"); + indentLevel++; + return this; + } + + /** + * Begins a for loop. + * + * @param init initialization expression + * @param condition loop condition + * @param update update expression + * @return this builder for chaining + */ + public JavaCodeBuilder beginFor(String init, String condition, String update) { + indent(); + code.append("for (") + .append(init) + .append("; ") + .append(condition) + .append("; ") + .append(update) + .append(") {\n"); + indentLevel++; + return this; + } + + /** Ends a for loop. */ + public JavaCodeBuilder endFor() { + indentLevel--; + indent(); + code.append("}\n"); + return this; + } + + // ==================== Statements ==================== + + /** + * Adds a statement with semicolon. + * + * @param statement the statement (without semicolon) + * @return this builder for chaining + */ + public JavaCodeBuilder stmt(String statement) { + indent(); + code.append(statement).append(";\n"); + return this; + } + + /** + * Adds a return statement. + * + * @param expression the return expression + * @return this builder for chaining + */ + public JavaCodeBuilder returnStmt(String expression) { + indent(); + code.append("return ").append(expression).append(";\n"); + return this; + } + + /** + * Adds a variable declaration with primitive type. + * + * @param type the primitive type + * @param name the variable name + * @param value the initial value expression + * @return this builder for chaining + */ + public JavaCodeBuilder declare(PrimitiveType type, String name, String value) { + return declare(type.toString(), name, value); + } + + /** + * Adds a variable declaration. + * + * @param type the variable type + * @param name the variable name + * @param value the initial value expression + * @return this builder for chaining + */ + public JavaCodeBuilder declare(String type, String name, String value) { + indent(); + code.append(type).append(" ").append(name).append(" = ").append(value).append(";\n"); + return this; + } + + /** + * Adds an assignment statement. + * + * @param variable the variable name + * @param value the value expression + * @return this builder for chaining + */ + public JavaCodeBuilder assign(String variable, String value) { + indent(); + code.append(variable).append(" = ").append(value).append(";\n"); + return this; + } + + /** + * Adds a continue statement. + * + * @return this builder for chaining + */ + public JavaCodeBuilder continueStmt() { + indent(); + code.append("continue;\n"); + return this; + } + + /** + * Adds a break statement. + * + * @return this builder for chaining + */ + public JavaCodeBuilder breakStmt() { + indent(); + code.append("break;\n"); + return this; + } + + // ==================== Raw Code ==================== + + /** + * Appends raw code with current indentation. + * + * @param rawCode the raw code to append + * @return this builder for chaining + */ + public JavaCodeBuilder raw(String rawCode) { + if (rawCode != null && !rawCode.isEmpty()) { + for (String line : rawCode.split("\n")) { + if (!line.trim().isEmpty()) { + indent(); + code.append(line.trim()).append("\n"); + } + } + } + return this; + } + + /** + * Appends raw code without any indentation processing. + * + * @param rawCode the raw code to append + * @return this builder for chaining + */ + public JavaCodeBuilder rawUnindented(String rawCode) { + if (rawCode != null && !rawCode.isEmpty()) { + code.append(rawCode); + if (!rawCode.endsWith("\n")) { + code.append("\n"); + } + } + return this; + } + + /** + * Appends a pre-formatted code block, adding current indentation to each line. + * + *

Unlike {@link #raw(String)} which trims lines, this method preserves the relative + * indentation within the code block while adding the current indent level as a base. + * + * @param codeBlock the code block to append + * @return this builder for chaining + */ + public JavaCodeBuilder rawBlock(String codeBlock) { + if (codeBlock == null || codeBlock.isEmpty()) { + return this; + } + for (String line : codeBlock.split("\n", -1)) { + if (line.isEmpty()) { + code.append("\n"); + } else { + indent(); + code.append(line).append("\n"); + } + } + return this; + } + + /** Adds a blank line. */ + public JavaCodeBuilder newLine() { + code.append("\n"); + return this; + } + + // ==================== Build ==================== + + /** + * Builds and returns the generated code. + * + * @return the generated Java source code + */ + public String build() { + return code.toString(); + } + + @Override + public String toString() { + return build(); + } + + // ==================== Internal ==================== + + /** Pre-computed indent strings for common levels to avoid repeated string concatenation. */ + private static final String[] INDENT_CACHE = new String[16]; + + static { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < INDENT_CACHE.length; i++) { + INDENT_CACHE[i] = sb.toString(); + sb.append(INDENT); + } + } + + private void indent() { + if (indentLevel < INDENT_CACHE.length) { + code.append(INDENT_CACHE[indentLevel]); + } else { + // Fallback for deep nesting (unlikely in practice) + for (int i = 0; i < indentLevel; i++) { + code.append(INDENT); + } + } + } +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/generator/EqualiserCodeGenerator.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/generator/EqualiserCodeGenerator.java new file mode 100644 index 0000000000..79542392b5 --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/generator/EqualiserCodeGenerator.java @@ -0,0 +1,804 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen.generator; + +import org.apache.fluss.codegen.CodeGenException; +import org.apache.fluss.codegen.CodeGeneratorContext; +import org.apache.fluss.codegen.GeneratedClass; +import org.apache.fluss.codegen.JavaCodeBuilder; +import org.apache.fluss.codegen.JavaCodeBuilder.Modifier; +import org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType; +import org.apache.fluss.codegen.types.RecordEqualiser; +import org.apache.fluss.row.BinaryArray; +import org.apache.fluss.row.BinaryRow; +import org.apache.fluss.row.BinaryString; +import org.apache.fluss.row.Decimal; +import org.apache.fluss.row.InternalArray; +import org.apache.fluss.row.InternalMap; +import org.apache.fluss.row.InternalRow; +import org.apache.fluss.row.TimestampLtz; +import org.apache.fluss.row.TimestampNtz; +import org.apache.fluss.types.DataType; +import org.apache.fluss.types.DataTypeChecks; +import org.apache.fluss.types.DataTypeRoot; +import org.apache.fluss.utils.TypeUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.FINAL; +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.PRIVATE; +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.PUBLIC; +import static org.apache.fluss.codegen.JavaCodeBuilder.Param.of; +import static org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType.BOOLEAN; +import static org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType.INT; +import static org.apache.fluss.codegen.JavaCodeBuilder.arrayOf; +import static org.apache.fluss.codegen.JavaCodeBuilder.typeOf; + +/** + * Code generator for {@link RecordEqualiser} using recursive descent approach. + * + *

The generator recursively descends into nested types (Row, Array, Map) to generate + * type-specific comparison code. The core method {@link #genEqualsExpr} dispatches to type-specific + * generators based on the data type category. + * + *

Recursive Descent Structure

+ * + *
+ * genClass()
+ *   ├── genMembers()
+ *   ├── genConstructor()
+ *   ├── genEqualsMethod()
+ *   └── genFieldMethods()
+ *         └── genFieldMethod()
+ *               ├── genNullCheck()
+ *               └── genEqualsExpr()  ← core recursive dispatch
+ *                     ├── Primitive  → "left == right"
+ *                     ├── Binary     → "Arrays.equals(left, right)"
+ *                     ├── Comparable → "left.compareTo(right) == 0"
+ *                     ├── Object     → "left.equals(right)"
+ *                     └── Composite  → recursive descent:
+ *                           ├── genRowEquals()   → nested EqualiserCodeGenerator
+ *                           ├── genArrayEquals() → genArrayEqualsMethod()
+ *                           │     └── genArrayElemComparison()
+ *                           │           └── genNotEqualsExpr() → genEqualsExpr()
+ *                           └── genMapEquals()   → genMapEqualsMethod()
+ *                                 ├── genMapEntryComparison()
+ *                                 │     └── genEqualsExpr() for key
+ *                                 └── genMapValueComparison()
+ *                                       └── genNotEqualsExpr() → genEqualsExpr()
+ * 
+ * + *

Generated Code Example

+ * + *

For a schema with fields: {@code (id INT, name STRING, tags ARRAY, metadata + * MAP, address ROW)}, the generated code is: + * + *

{@code
+ * public final class RecordEqualiser$1 implements RecordEqualiser {
+ *
+ *     // ==================== Member Fields ====================
+ *     // Nested equaliser for ROW field, lazily compiled
+ *     private GeneratedClass nestedEqualiser$1;
+ *     private RecordEqualiser rowEq$1;
+ *
+ *     // ==================== Constructor ====================
+ *     public RecordEqualiser$1(Object[] references) {
+ *         nestedEqualiser$1 = (GeneratedClass) references[0];
+ *         rowEq$1 = (RecordEqualiser) nestedEqualiser$1.newInstance(
+ *             this.getClass().getClassLoader());
+ *     }
+ *
+ *     // ==================== Main Equals Method ====================
+ *     @Override
+ *     public boolean equals(InternalRow left, InternalRow right) {
+ *         // Fast path: BinaryRow direct comparison (only when no projection)
+ *         if (left instanceof BinaryRow && right instanceof BinaryRow) {
+ *             return left.equals(right);
+ *         }
+ *         // Field-by-field comparison with short-circuit evaluation
+ *         boolean result = true;
+ *         result = result && equalsField0(left, right);
+ *         result = result && equalsField1(left, right);
+ *         result = result && equalsField2(left, right);
+ *         result = result && equalsField3(left, right);
+ *         result = result && equalsField4(left, right);
+ *         return result;
+ *     }
+ *
+ *     // ==================== Field 0: INT (Primitive) ====================
+ *     private boolean equalsField0(InternalRow left, InternalRow right) {
+ *         boolean leftNull = left.isNullAt(0);
+ *         boolean rightNull = right.isNullAt(0);
+ *         if (leftNull && rightNull) {
+ *             return true;
+ *         }
+ *         if (leftNull || rightNull) {
+ *             return false;
+ *         }
+ *         int leftVal = left.getInt(0);
+ *         int rightVal = right.getInt(0);
+ *         return leftVal == rightVal;  // Primitive: direct ==
+ *     }
+ *
+ *     // ==================== Field 1: STRING (Object) ====================
+ *     private boolean equalsField1(InternalRow left, InternalRow right) {
+ *         boolean leftNull = left.isNullAt(1);
+ *         boolean rightNull = right.isNullAt(1);
+ *         if (leftNull && rightNull) {
+ *             return true;
+ *         }
+ *         if (leftNull || rightNull) {
+ *             return false;
+ *         }
+ *         BinaryString leftVal = ((BinaryString) left.getString(1));
+ *         BinaryString rightVal = ((BinaryString) right.getString(1));
+ *         return leftVal.equals(rightVal);  // Object: .equals()
+ *     }
+ *
+ *     // ==================== Field 2: ARRAY ====================
+ *     private boolean equalsField2(InternalRow left, InternalRow right) {
+ *         boolean leftNull = left.isNullAt(2);
+ *         boolean rightNull = right.isNullAt(2);
+ *         if (leftNull && rightNull) {
+ *             return true;
+ *         }
+ *         if (leftNull || rightNull) {
+ *             return false;
+ *         }
+ *         InternalArray leftVal = left.getArray(2);
+ *         InternalArray rightVal = right.getArray(2);
+ *         return arrEq$1(leftVal, rightVal);  // Delegate to array method
+ *     }
+ *
+ *     // Array comparison helper with BinaryArray fast path
+ *     private boolean arrEq$1(InternalArray left, InternalArray right) {
+ *         // Fast path: BinaryArray direct comparison
+ *         if (left instanceof BinaryArray && right instanceof BinaryArray) {
+ *             return left.equals(right);
+ *         }
+ *         // Size check
+ *         if (left.size() != right.size()) {
+ *             return false;
+ *         }
+ *         // Element-by-element comparison
+ *         for (int i = 0; i < left.size(); i++) {
+ *             if (left.isNullAt(i) && right.isNullAt(i)) {
+ *                 continue;
+ *             }
+ *             if (left.isNullAt(i) || right.isNullAt(i)) {
+ *                 return false;
+ *             }
+ *             BinaryString l = left.getString(i);
+ *             BinaryString r = right.getString(i);
+ *             if (!l.equals(r)) {  // Recursive: element type comparison
+ *                 return false;
+ *             }
+ *         }
+ *         return true;
+ *     }
+ *
+ *     // ==================== Field 3: MAP ====================
+ *     private boolean equalsField3(InternalRow left, InternalRow right) {
+ *         boolean leftNull = left.isNullAt(3);
+ *         boolean rightNull = right.isNullAt(3);
+ *         if (leftNull && rightNull) {
+ *             return true;
+ *         }
+ *         if (leftNull || rightNull) {
+ *             return false;
+ *         }
+ *         InternalMap leftVal = left.getMap(3);
+ *         InternalMap rightVal = right.getMap(3);
+ *         return mapEq$1(leftVal, rightVal);  // Delegate to map method
+ *     }
+ *
+ *     // Map comparison helper with O(n²) key lookup
+ *     private boolean mapEq$1(InternalMap left, InternalMap right) {
+ *         // Size check
+ *         if (left.size() != right.size()) {
+ *             return false;
+ *         }
+ *         // Extract key/value arrays
+ *         InternalArray lk = left.keyArray();
+ *         InternalArray lv = left.valueArray();
+ *         InternalArray rk = right.keyArray();
+ *         InternalArray rv = right.valueArray();
+ *         // O(n²) comparison: for each left entry, find matching right entry
+ *         for (int i = 0; i < left.size(); i++) {
+ *             BinaryString lKey = lk.getString(i);
+ *             boolean found = false;
+ *             for (int j = 0; j < right.size(); j++) {
+ *                 BinaryString rKey = rk.getString(j);
+ *                 if (lKey.equals(rKey)) {  // Recursive: key type comparison
+ *                     // Key matched, compare values
+ *                     if (lv.isNullAt(i) && rv.isNullAt(j)) {
+ *                         found = true;
+ *                         break;
+ *                     }
+ *                     if (lv.isNullAt(i) || rv.isNullAt(j)) {
+ *                         return false;
+ *                     }
+ *                     int lVal = lv.getInt(i);
+ *                     int rVal = rv.getInt(j);
+ *                     if (lVal != rVal) {  // Recursive: value type comparison
+ *                         return false;
+ *                     }
+ *                     found = true;
+ *                     break;
+ *                 }
+ *             }
+ *             if (!found) {
+ *                 return false;
+ *             }
+ *         }
+ *         return true;
+ *     }
+ *
+ *     // ==================== Field 4: ROW ====================
+ *     private boolean equalsField4(InternalRow left, InternalRow right) {
+ *         boolean leftNull = left.isNullAt(4);
+ *         boolean rightNull = right.isNullAt(4);
+ *         if (leftNull && rightNull) {
+ *             return true;
+ *         }
+ *         if (leftNull || rightNull) {
+ *             return false;
+ *         }
+ *         InternalRow leftVal = left.getRow(4, 2);
+ *         InternalRow rightVal = right.getRow(4, 2);
+ *         return rowEq$1.equals(leftVal, rightVal);  // Delegate to nested equaliser
+ *     }
+ * }
+ * }
+ * + *

Optimization Strategies

+ * + *
    + *
  • BinaryRow fast path: When both rows are BinaryRow instances (and no projection), + * direct byte-level comparison via {@code equals()} avoids field-by-field overhead. + *
  • BinaryArray fast path: Similar optimization for array comparisons. + *
  • Short-circuit evaluation: Field comparisons are chained with {@code &&} to exit + * early on first mismatch. + *
  • Null handling: Null checks are performed before value access to avoid NPE and + * correctly handle null equality semantics. + *
  • Type-specific comparison: Primitives use {@code ==}, binary uses {@code + * Arrays.equals()}, comparable types use {@code compareTo()}, objects use {@code equals()}. + *
  • Nested equaliser reuse: For ROW fields, a separate equaliser is generated once and + * reused across comparisons. + *
+ * + *

Supporting Methods

+ * + *
    + *
  • Type classification: {@link org.apache.fluss.utils.TypeUtils#isPrimitive}, {@link + * org.apache.fluss.utils.TypeUtils#isBinary}, {@link + * org.apache.fluss.utils.TypeUtils#isComparable} + *
  • Type mapping: {@link #toJavaType} + *
  • Field/element access: {@link #genAccess} (unified for Row and Array) + *
+ */ +public class EqualiserCodeGenerator { + + // ==================== Type Name Constants ==================== + + private static final String T_RECORD_EQUALISER = typeOf(RecordEqualiser.class); + private static final String T_ROW_DATA = typeOf(InternalRow.class); + private static final String T_BINARY_ROW = typeOf(BinaryRow.class); + private static final String T_BINARY_STRING = typeOf(BinaryString.class); + private static final String T_DECIMAL = typeOf(Decimal.class); + private static final String T_TIMESTAMP_NTZ = typeOf(TimestampNtz.class); + private static final String T_TIMESTAMP_LTZ = typeOf(TimestampLtz.class); + private static final String T_INTERNAL_ARRAY = typeOf(InternalArray.class); + private static final String T_INTERNAL_MAP = typeOf(InternalMap.class); + private static final String T_BINARY_ARRAY = typeOf(BinaryArray.class); + private static final String T_GENERATED_CLASS = typeOf(GeneratedClass.class); + + private final DataType[] fieldTypes; + private final int[] fields; + + // ==================== Constructor ==================== + + public EqualiserCodeGenerator(DataType[] fieldTypes) { + this(fieldTypes, IntStream.range(0, fieldTypes.length).toArray()); + } + + public EqualiserCodeGenerator(DataType[] fieldTypes, int[] fields) { + this.fieldTypes = fieldTypes; + this.fields = fields; + } + + // ==================== Public API ==================== + + public GeneratedClass generateRecordEqualiser(String name) { + CodeGeneratorContext ctx = new CodeGeneratorContext(); + String className = CodeGeneratorContext.newName(name); + String code = genClass(ctx, className); + return new GeneratedClass(className, code, ctx.getReferences()); + } + + // ==================== Class Structure Generation ==================== + + private String genClass(CodeGeneratorContext ctx, String className) { + // Generate field methods first to collect members + List fieldMethods = genFieldMethods(ctx); + + JavaCodeBuilder b = new JavaCodeBuilder(); + b.beginClass(new Modifier[] {PUBLIC, FINAL}, className, T_RECORD_EQUALISER); + + genMembers(b, ctx); + genConstructor(b, ctx, className); + genEqualsMethod(b); + appendFieldMethods(b, fieldMethods); + + b.endClass(); + return b.build(); + } + + private void genMembers(JavaCodeBuilder b, CodeGeneratorContext ctx) { + String code = ctx.reuseMemberCode(); + if (!code.isEmpty()) { + b.rawBlock(code); + b.newLine(); + } + } + + private void genConstructor(JavaCodeBuilder b, CodeGeneratorContext ctx, String className) { + b.beginConstructor(PUBLIC, className, of(arrayOf(Object.class), "references")); + String code = ctx.reuseInitCode(); + if (!code.isEmpty()) { + b.raw(code); + } + b.endConstructor(); + } + + private void genEqualsMethod(JavaCodeBuilder b) { + boolean hasProjection = fieldTypes.length > fields.length; + + b.newLine(); + b.override(); + b.beginMethod(PUBLIC, BOOLEAN, "equals", of(T_ROW_DATA, "left"), of(T_ROW_DATA, "right")); + + // BinaryRow fast path + if (!hasProjection) { + b.beginIf("left instanceof " + T_BINARY_ROW + " && right instanceof " + T_BINARY_ROW); + b.returnStmt("left.equals(right)"); + b.endIf(); + } + + // Field comparison + b.declare(BOOLEAN, "result", "true"); + for (int idx : fields) { + b.assign("result", "result && equalsField" + idx + "(left, right)"); + } + b.returnStmt("result"); + b.endMethod(); + } + + private List genFieldMethods(CodeGeneratorContext ctx) { + List methods = new ArrayList(); + for (int idx : fields) { + methods.add(genFieldMethod(ctx, idx)); + } + return methods; + } + + private void appendFieldMethods(JavaCodeBuilder b, List methods) { + for (String method : methods) { + b.newLine(); + b.rawBlock(method); + } + } + + // ==================== Field Comparison Method ==================== + + private String genFieldMethod(CodeGeneratorContext ctx, int idx) { + DataType type = fieldTypes[idx]; + String javaType = toJavaType(type); + + JavaCodeBuilder b = new JavaCodeBuilder(); + b.beginMethod( + PRIVATE, + BOOLEAN, + "equalsField" + idx, + of(T_ROW_DATA, "left"), + of(T_ROW_DATA, "right")); + + // Null check + genNullCheck(b, idx); + + // Read values + b.declare(javaType, "leftVal", genFieldAccess("left", idx, type)); + b.declare(javaType, "rightVal", genFieldAccess("right", idx, type)); + + // Compare + b.returnStmt(genEqualsExpr(ctx, type, "leftVal", "rightVal")); + + b.endMethod(); + return b.build(); + } + + private void genNullCheck(JavaCodeBuilder b, int idx) { + b.declare(BOOLEAN, "leftNull", "left.isNullAt(" + idx + ")"); + b.declare(BOOLEAN, "rightNull", "right.isNullAt(" + idx + ")"); + b.beginIf("leftNull && rightNull"); + b.returnStmt("true"); + b.endIf(); + b.beginIf("leftNull || rightNull"); + b.returnStmt("false"); + b.endIf(); + } + + // ==================== Equals Expression (Recursive Descent) ==================== + + /** + * Generates equals expression by recursively descending into the type structure. This is the + * core recursive descent method. + */ + private String genEqualsExpr( + CodeGeneratorContext ctx, DataType type, String left, String right) { + DataTypeRoot root = type.getTypeRoot(); + + // Primitive: == + if (TypeUtils.isPrimitive(root)) { + return left + " == " + right; + } + + // Binary: Arrays.equals + if (TypeUtils.isBinary(root)) { + return "java.util.Arrays.equals(" + left + ", " + right + ")"; + } + + // Comparable: compareTo + if (TypeUtils.isComparable(root)) { + return left + ".compareTo(" + right + ") == 0"; + } + + // Composite: recursive descent + if (root == DataTypeRoot.ROW) { + return genRowEquals(ctx, type, left, right); + } + if (root == DataTypeRoot.ARRAY) { + return genArrayEquals(ctx, type, left, right); + } + if (root == DataTypeRoot.MAP) { + return genMapEquals(ctx, type, left, right); + } + + // Object: equals() + return left + ".equals(" + right + ")"; + } + + /** Generates not-equals expression (inverse of genEqualsExpr). */ + private String genNotEqualsExpr( + CodeGeneratorContext ctx, DataType type, String left, String right) { + DataTypeRoot root = type.getTypeRoot(); + + if (TypeUtils.isPrimitive(root)) { + return left + " != " + right; + } + if (TypeUtils.isBinary(root)) { + return "!java.util.Arrays.equals(" + left + ", " + right + ")"; + } + if (TypeUtils.isComparable(root)) { + return left + ".compareTo(" + right + ") != 0"; + } + + // For composite and object types, negate the equals expression + return "!" + genEqualsExpr(ctx, type, left, right); + } + + // ==================== Row Equals ==================== + + private String genRowEquals( + CodeGeneratorContext ctx, DataType rowType, String left, String right) { + // Generate nested equaliser + List nestedTypes = DataTypeChecks.getFieldTypes(rowType); + EqualiserCodeGenerator nestedGen = + new EqualiserCodeGenerator(nestedTypes.toArray(new DataType[0])); + GeneratedClass generated = + nestedGen.generateRecordEqualiser("nestedEqualiser"); + + // Register in context + String genTerm = ctx.addReusableObject(generated, "nestedEqualiser", T_GENERATED_CLASS); + String instTerm = CodeGeneratorContext.newName("rowEq"); + + ctx.addReusableMember("private " + T_RECORD_EQUALISER + " " + instTerm + ";"); + ctx.addReusableInitStatement( + instTerm + + " = (" + + T_RECORD_EQUALISER + + ") " + + genTerm + + ".newInstance(this.getClass().getClassLoader());"); + + return instTerm + ".equals(" + left + ", " + right + ")"; + } + + // ==================== Array Equals ==================== + + private String genArrayEquals( + CodeGeneratorContext ctx, DataType arrayType, String left, String right) { + DataType elemType = DataTypeChecks.getArrayElementType(arrayType); + String methodName = CodeGeneratorContext.newName("arrEq"); + + ctx.addReusableMember(genArrayEqualsMethod(ctx, methodName, elemType)); + return methodName + "(" + left + ", " + right + ")"; + } + + private String genArrayEqualsMethod( + CodeGeneratorContext ctx, String methodName, DataType elemType) { + String elemJavaType = toJavaType(elemType); + + JavaCodeBuilder b = new JavaCodeBuilder(); + b.beginMethod( + PRIVATE, + BOOLEAN, + methodName, + of(T_INTERNAL_ARRAY, "left"), + of(T_INTERNAL_ARRAY, "right")); + + // Fast path + b.beginIf("left instanceof " + T_BINARY_ARRAY + " && right instanceof " + T_BINARY_ARRAY); + b.returnStmt("left.equals(right)"); + b.endIf(); + + // Size check + b.beginIf("left.size() != right.size()"); + b.returnStmt("false"); + b.endIf(); + + // Element loop + b.beginFor("int i = 0", "i < left.size()", "i++"); + genArrayElemComparison(b, ctx, elemType, elemJavaType); + b.endFor(); + + b.returnStmt("true"); + b.endMethod(); + return b.build(); + } + + private void genArrayElemComparison( + JavaCodeBuilder b, CodeGeneratorContext ctx, DataType elemType, String elemJavaType) { + // Null check + b.beginIf("left.isNullAt(i) && right.isNullAt(i)"); + b.continueStmt(); + b.endIf(); + b.beginIf("left.isNullAt(i) || right.isNullAt(i)"); + b.returnStmt("false"); + b.endIf(); + + // Read and compare (recursive descent into element type) + b.declare(elemJavaType, "l", genArrayAccess("left", "i", elemType)); + b.declare(elemJavaType, "r", genArrayAccess("right", "i", elemType)); + b.beginIf(genNotEqualsExpr(ctx, elemType, "l", "r")); + b.returnStmt("false"); + b.endIf(); + } + + // ==================== Map Equals ==================== + + private String genMapEquals( + CodeGeneratorContext ctx, DataType mapType, String left, String right) { + DataType keyType = DataTypeChecks.getMapKeyType(mapType); + DataType valType = DataTypeChecks.getMapValueType(mapType); + String methodName = CodeGeneratorContext.newName("mapEq"); + + ctx.addReusableMember(genMapEqualsMethod(ctx, methodName, keyType, valType)); + return methodName + "(" + left + ", " + right + ")"; + } + + private String genMapEqualsMethod( + CodeGeneratorContext ctx, String methodName, DataType keyType, DataType valType) { + String keyJavaType = toJavaType(keyType); + String valJavaType = toJavaType(valType); + + JavaCodeBuilder b = new JavaCodeBuilder(); + b.beginMethod( + PRIVATE, + BOOLEAN, + methodName, + of(T_INTERNAL_MAP, "left"), + of(T_INTERNAL_MAP, "right")); + + // Size check + b.beginIf("left.size() != right.size()"); + b.returnStmt("false"); + b.endIf(); + + // Get arrays + b.declare(T_INTERNAL_ARRAY, "lk", "left.keyArray()"); + b.declare(T_INTERNAL_ARRAY, "lv", "left.valueArray()"); + b.declare(T_INTERNAL_ARRAY, "rk", "right.keyArray()"); + b.declare(T_INTERNAL_ARRAY, "rv", "right.valueArray()"); + + // O(n²) comparison + b.beginFor("int i = 0", "i < left.size()", "i++"); + genMapEntryComparison(b, ctx, keyType, valType, keyJavaType, valJavaType); + b.endFor(); + + b.returnStmt("true"); + b.endMethod(); + return b.build(); + } + + private void genMapEntryComparison( + JavaCodeBuilder b, + CodeGeneratorContext ctx, + DataType keyType, + DataType valType, + String keyJavaType, + String valJavaType) { + b.declare(keyJavaType, "lKey", genArrayAccess("lk", "i", keyType)); + b.declare(BOOLEAN, "found", "false"); + + // Inner loop + b.beginFor("int j = 0", "j < right.size()", "j++"); + b.declare(keyJavaType, "rKey", genArrayAccess("rk", "j", keyType)); + + // Key match (recursive descent into key type) + b.beginIf(genEqualsExpr(ctx, keyType, "lKey", "rKey")); + genMapValueComparison(b, ctx, valType, valJavaType); + b.endIf(); + + b.endFor(); + + b.beginIf("!found"); + b.returnStmt("false"); + b.endIf(); + } + + private void genMapValueComparison( + JavaCodeBuilder b, CodeGeneratorContext ctx, DataType valType, String valJavaType) { + // Null check + b.beginIf("lv.isNullAt(i) && rv.isNullAt(j)"); + b.assign("found", "true"); + b.breakStmt(); + b.endIf(); + b.beginIf("lv.isNullAt(i) || rv.isNullAt(j)"); + b.returnStmt("false"); + b.endIf(); + + // Value comparison (recursive descent into value type) + b.declare(valJavaType, "lVal", genArrayAccess("lv", "i", valType)); + b.declare(valJavaType, "rVal", genArrayAccess("rv", "j", valType)); + b.beginIf(genNotEqualsExpr(ctx, valType, "lVal", "rVal")); + b.returnStmt("false"); + b.endIf(); + + b.assign("found", "true"); + b.breakStmt(); + } + + // ==================== Type Mapping ==================== + + private String toJavaType(DataType type) { + DataTypeRoot root = type.getTypeRoot(); + switch (root) { + case BOOLEAN: + return PrimitiveType.BOOLEAN.toString(); + case TINYINT: + return PrimitiveType.BYTE.toString(); + case SMALLINT: + return PrimitiveType.SHORT.toString(); + case INTEGER: + case DATE: + case TIME_WITHOUT_TIME_ZONE: + return INT.toString(); + case BIGINT: + return PrimitiveType.LONG.toString(); + case FLOAT: + return PrimitiveType.FLOAT.toString(); + case DOUBLE: + return PrimitiveType.DOUBLE.toString(); + case CHAR: + case STRING: + return T_BINARY_STRING; + case BINARY: + case BYTES: + return "byte[]"; + case DECIMAL: + return T_DECIMAL; + case TIMESTAMP_WITHOUT_TIME_ZONE: + return T_TIMESTAMP_NTZ; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return T_TIMESTAMP_LTZ; + case ARRAY: + return T_INTERNAL_ARRAY; + case MAP: + return T_INTERNAL_MAP; + case ROW: + return T_ROW_DATA; + default: + throw new CodeGenException("Unsupported type: " + type); + } + } + + // ==================== Field/Element Access ==================== + + private String genFieldAccess(String row, int idx, DataType type) { + return genAccess(row, String.valueOf(idx), type, true); + } + + private String genArrayAccess(String arr, String idx, DataType type) { + return genAccess(arr, idx, type, false); + } + + /** + * Unified access code generation for both row fields and array elements. + * + * @param container the row or array variable name + * @param idx the index expression + * @param type the data type + * @param isRow true for row field access, false for array element access + */ + private String genAccess(String container, String idx, DataType type, boolean isRow) { + DataTypeRoot root = type.getTypeRoot(); + switch (root) { + case BOOLEAN: + return container + ".getBoolean(" + idx + ")"; + case TINYINT: + return container + ".getByte(" + idx + ")"; + case SMALLINT: + return container + ".getShort(" + idx + ")"; + case INTEGER: + case DATE: + case TIME_WITHOUT_TIME_ZONE: + return container + ".getInt(" + idx + ")"; + case BIGINT: + return container + ".getLong(" + idx + ")"; + case FLOAT: + return container + ".getFloat(" + idx + ")"; + case DOUBLE: + return container + ".getDouble(" + idx + ")"; + case CHAR: + int charLen = DataTypeChecks.getLength(type); + String charAccess = container + ".getChar(" + idx + ", " + charLen + ")"; + return isRow ? "((" + T_BINARY_STRING + ") " + charAccess + ")" : charAccess; + case STRING: + String strAccess = container + ".getString(" + idx + ")"; + return isRow ? "((" + T_BINARY_STRING + ") " + strAccess + ")" : strAccess; + case BINARY: + int binLen = DataTypeChecks.getLength(type); + return container + ".getBinary(" + idx + ", " + binLen + ")"; + case BYTES: + return container + ".getBytes(" + idx + ")"; + case DECIMAL: + int p = DataTypeChecks.getPrecision(type); + int s = DataTypeChecks.getScale(type); + return container + ".getDecimal(" + idx + ", " + p + ", " + s + ")"; + case TIMESTAMP_WITHOUT_TIME_ZONE: + int ntzP = DataTypeChecks.getPrecision(type); + return container + ".getTimestampNtz(" + idx + ", " + ntzP + ")"; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + int ltzP = DataTypeChecks.getPrecision(type); + return container + ".getTimestampLtz(" + idx + ", " + ltzP + ")"; + case ARRAY: + return container + ".getArray(" + idx + ")"; + case MAP: + return container + ".getMap(" + idx + ")"; + case ROW: + int fc = DataTypeChecks.getFieldCount(type); + return container + ".getRow(" + idx + ", " + fc + ")"; + default: + throw new CodeGenException("Unsupported type: " + type); + } + } +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/generator/package-info.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/generator/package-info.java new file mode 100644 index 0000000000..c1d4beed4c --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/generator/package-info.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Concrete code generators for specific use cases. + * + *

This package contains the actual code generator implementations that produce generated classes + * at runtime. Each generator is responsible for generating code for a specific purpose. + * + *

    + *
  • {@link org.apache.fluss.codegen.generator.EqualiserCodeGenerator} - Generates {@link + * org.apache.fluss.codegen.types.RecordEqualiser} implementations for comparing InternalRow + * instances + *
+ * + *

To add a new code generator: + * + *

    + *
  1. Define the interface in {@code org.apache.fluss.codegen.types} + *
  2. Create the generator class in this package + *
  3. Use {@link org.apache.fluss.codegen.CodeGeneratorContext} for managing reusable code + *
  4. Use {@link org.apache.fluss.codegen.JavaCodeBuilder} for building Java source code + *
  5. Return a {@link org.apache.fluss.codegen.GeneratedClass} from the generator + *
+ */ +package org.apache.fluss.codegen.generator; diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/package-info.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/package-info.java new file mode 100644 index 0000000000..279ec9c530 --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/package-info.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Code generation framework for Fluss. + * + *

This package provides the core infrastructure for runtime code generation using Janino + * compiler. It enables generating optimized Java code at runtime for performance-critical + * operations. + * + *

Core Components

+ * + *
    + *
  • {@link org.apache.fluss.codegen.CodeGeneratorContext} - Manages reusable code fragments and + * member variables + *
  • {@link org.apache.fluss.codegen.JavaCodeBuilder} - Type-safe builder for constructing Java + * source code + *
  • {@link org.apache.fluss.codegen.CompileUtils} - Compiles generated source code using Janino + *
  • {@link org.apache.fluss.codegen.GeneratedClass} - Wrapper for generated class with source + * code and compiled class + *
  • {@link org.apache.fluss.codegen.CodeGenException} - Exception for code generation failures + *
+ * + *

Sub-packages

+ * + *
    + *
  • {@code org.apache.fluss.codegen.types} - API interfaces for generated classes + *
  • {@code org.apache.fluss.codegen.generator} - Concrete code generator implementations + *
+ * + *

Usage Example

+ * + *
{@code
+ * // Use a specific generator
+ * EqualiserCodeGenerator generator = new EqualiserCodeGenerator(rowType);
+ * GeneratedClass generated = generator.generate();
+ *
+ * // Get the compiled instance
+ * RecordEqualiser equaliser = generated.newInstance(classLoader);
+ * boolean equal = equaliser.equals(row1, row2);
+ * }
+ * + * @see org.apache.fluss.codegen.generator.EqualiserCodeGenerator + * @see org.apache.fluss.codegen.types.RecordEqualiser + */ +package org.apache.fluss.codegen; diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/types/RecordEqualiser.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/types/RecordEqualiser.java new file mode 100644 index 0000000000..277939bc95 --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/types/RecordEqualiser.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen.types; + +import org.apache.fluss.row.InternalRow; + +import java.io.Serializable; + +/** + * Record equaliser for InternalRow which can compare two InternalRow instances and returns whether + * they are equal. + * + *

This interface is implemented by generated classes at runtime for high-performance record + * comparison without boxing overhead. + */ +public interface RecordEqualiser extends Serializable { + + /** + * Returns {@code true} if the rows are equal to each other and {@code false} otherwise. + * + * @param row1 the first row to compare + * @param row2 the second row to compare + * @return true if the rows are equal, false otherwise + */ + boolean equals(InternalRow row1, InternalRow row2); +} diff --git a/fluss-codegen/src/main/java/org/apache/fluss/codegen/types/package-info.java b/fluss-codegen/src/main/java/org/apache/fluss/codegen/types/package-info.java new file mode 100644 index 0000000000..274746599e --- /dev/null +++ b/fluss-codegen/src/main/java/org/apache/fluss/codegen/types/package-info.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * API interfaces for code generation. + * + *

This package contains stable interfaces that define the contract for runtime-generated + * classes. These interfaces are implemented by dynamically generated code at runtime. + * + *

    + *
  • {@link org.apache.fluss.codegen.types.RecordEqualiser} - Interface for comparing two + * InternalRow instances + *
+ * + *

These interfaces are stable and should not change frequently. New generated class types should + * add their interfaces here. + */ +package org.apache.fluss.codegen.types; diff --git a/fluss-codegen/src/main/resources/META-INF/NOTICE b/fluss-codegen/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..5f34d2f3fa --- /dev/null +++ b/fluss-codegen/src/main/resources/META-INF/NOTICE @@ -0,0 +1,11 @@ +fluss-codegen +Copyright 2025-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This project bundles the following dependencies under the BSD 3-clause license. +You find them under licenses/LICENSE.janino. + +- org.codehaus.janino:janino:3.1.9 +- org.codehaus.janino:commons-compiler:3.1.9 diff --git a/fluss-codegen/src/main/resources/META-INF/licenses/LICENSE.janino b/fluss-codegen/src/main/resources/META-INF/licenses/LICENSE.janino new file mode 100644 index 0000000000..ef871e2426 --- /dev/null +++ b/fluss-codegen/src/main/resources/META-INF/licenses/LICENSE.janino @@ -0,0 +1,31 @@ +Janino - An embedded Java[TM] compiler + +Copyright (c) 2001-2016, Arno Unkrig +Copyright (c) 2015-2016 TIBCO Software Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + 3. Neither the name of JANINO nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/fluss-codegen/src/test/java/org/apache/fluss/codegen/CodeGenTestUtils.java b/fluss-codegen/src/test/java/org/apache/fluss/codegen/CodeGenTestUtils.java new file mode 100644 index 0000000000..71fe7bbbfc --- /dev/null +++ b/fluss-codegen/src/test/java/org/apache/fluss/codegen/CodeGenTestUtils.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Utilities for code generation tests with expected file comparison. + * + *

Supports two modes: + * + *

    + *
  • Normal mode: Compares generated code against expected files + *
  • Update mode: When {@code -Dcodegen.update.expected=true}, overwrites expected files + *
+ * + *

Expected files location: {@code src/test/resources/expected/} + */ +public final class CodeGenTestUtils { + + private static final String EXPECTED_DIR = "expected"; + private static final String UPDATE_PROPERTY = "codegen.update.expected"; + + private CodeGenTestUtils() {} + + /** + * Asserts that the generated code matches the expected file content. + * + * @param actual the actual generated code + * @param relativePath path relative to the expected directory + */ + public static void assertMatchesExpected(String actual, String relativePath) { + assertMatchesExpectedInternal(actual, relativePath, false); + } + + /** + * Asserts that the generated code matches the expected file content, with normalization. + * + *

Normalization: trims trailing whitespace, normalizes line endings to LF, ensures single + * trailing newline. + * + * @param actual the actual generated code + * @param relativePath path relative to the expected directory + */ + public static void assertMatchesExpectedNormalized(String actual, String relativePath) { + assertMatchesExpectedInternal(actual, relativePath, true); + } + + private static void assertMatchesExpectedInternal( + String actual, String relativePath, boolean normalize) { + String actualContent = normalize ? normalize(actual) : actual; + + if (shouldUpdateExpected()) { + updateExpectedFile(actualContent, relativePath); + return; + } + + String expected = readExpectedFile(relativePath); + String expectedContent = normalize ? normalize(expected) : expected; + + assertThat(actualContent) + .as("Generated code should match expected file: %s", relativePath) + .isEqualTo(expectedContent); + } + + private static String readExpectedFile(String relativePath) { + String resourcePath = EXPECTED_DIR + "/" + relativePath; + URL resource = CodeGenTestUtils.class.getClassLoader().getResource(resourcePath); + + if (resource == null) { + fail( + "Expected file not found: %s\n" + + "Create at: src/test/resources/%s\n" + + "Or run with -D%s=true to auto-generate", + resourcePath, resourcePath, UPDATE_PROPERTY); + } + + try (InputStream is = resource.openStream()) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int length; + while ((length = is.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString(StandardCharsets.UTF_8.name()); + } catch (IOException e) { + throw new RuntimeException("Failed to read expected file: " + resourcePath, e); + } + } + + private static boolean shouldUpdateExpected() { + return "true".equalsIgnoreCase(System.getProperty(UPDATE_PROPERTY)); + } + + private static void updateExpectedFile(String content, String relativePath) { + Path sourceRoot = findSourceRoot(); + Path expectedFile = + sourceRoot.resolve( + "fluss-codegen/src/test/resources/" + EXPECTED_DIR + "/" + relativePath); + + try { + Files.createDirectories(expectedFile.getParent()); + try (OutputStream os = Files.newOutputStream(expectedFile)) { + os.write(content.getBytes(StandardCharsets.UTF_8)); + } + System.out.println("[CodeGenTestUtils] Updated: " + expectedFile); + } catch (IOException e) { + throw new RuntimeException("Failed to update expected file: " + expectedFile, e); + } + } + + private static Path findSourceRoot() { + Path current = Paths.get("").toAbsolutePath(); + + // Walk up until we find fluss-codegen directory + for (Path path = current; path != null; path = path.getParent()) { + if (Files.exists(path.resolve("fluss-codegen"))) { + return path; + } + } + + throw new RuntimeException( + "Cannot find fluss source root from: " + + current + + "\nExpected file update requires running from the project directory."); + } + + private static String normalize(String code) { + if (code == null || code.isEmpty()) { + return "\n"; + } + + String[] lines = code.replace("\r\n", "\n").replace("\r", "\n").split("\n", -1); + StringBuilder sb = new StringBuilder(); + int lastNonEmpty = -1; + + // Single pass: build result and track last non-empty line + for (int i = 0; i < lines.length; i++) { + String trimmed = trimTrailing(lines[i]); + if (!trimmed.isEmpty()) { + lastNonEmpty = sb.length() + trimmed.length(); + } + if (i > 0) { + sb.append("\n"); + } + sb.append(trimmed); + } + + // Truncate to last non-empty content and add single trailing newline + if (lastNonEmpty > 0) { + sb.setLength(lastNonEmpty); + } + sb.append("\n"); + return sb.toString(); + } + + private static String trimTrailing(String s) { + int end = s.length(); + while (end > 0 && Character.isWhitespace(s.charAt(end - 1))) { + end--; + } + return end == s.length() ? s : s.substring(0, end); + } +} diff --git a/fluss-codegen/src/test/java/org/apache/fluss/codegen/CodeGeneratorContextTest.java b/fluss-codegen/src/test/java/org/apache/fluss/codegen/CodeGeneratorContextTest.java new file mode 100644 index 0000000000..92425d5db8 --- /dev/null +++ b/fluss-codegen/src/test/java/org/apache/fluss/codegen/CodeGeneratorContextTest.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CodeGeneratorContext}. + * + *

Test coverage includes: + * + *

    + *
  • Name generation uniqueness (including concurrent scenarios) + *
  • Member statement management + *
  • Reusable object handling (serialization, deep copy) + *
  • Init statement management + *
  • References array correctness + *
  • Edge cases and error handling + *
+ */ +public class CodeGeneratorContextTest { + + private CodeGeneratorContext context; + + @BeforeEach + public void setUp() { + context = new CodeGeneratorContext(); + } + + // ==================== Name Generation Tests ==================== + + @Test + public void testNewNameGeneratesUniqueNames() { + String name1 = CodeGeneratorContext.newName("field"); + String name2 = CodeGeneratorContext.newName("field"); + String name3 = CodeGeneratorContext.newName("field"); + + assertThat(name1).startsWith("field$"); + assertThat(name2).startsWith("field$"); + assertThat(name3).startsWith("field$"); + + // All names should be unique + assertThat(name1).isNotEqualTo(name2); + assertThat(name2).isNotEqualTo(name3); + assertThat(name1).isNotEqualTo(name3); + } + + @Test + public void testNewNameWithDifferentPrefixes() { + String name1 = CodeGeneratorContext.newName("left"); + String name2 = CodeGeneratorContext.newName("right"); + + assertThat(name1).startsWith("left$"); + assertThat(name2).startsWith("right$"); + assertThat(name1).isNotEqualTo(name2); + } + + @Test + public void testNewNameThreadSafety() throws InterruptedException { + int threadCount = 10; + int namesPerThread = 100; + Set allNames = new HashSet<>(); + CountDownLatch latch = new CountDownLatch(threadCount); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + for (int t = 0; t < threadCount; t++) { + executor.submit( + () -> { + try { + Set localNames = new HashSet<>(); + for (int i = 0; i < namesPerThread; i++) { + localNames.add(CodeGeneratorContext.newName("concurrent")); + } + synchronized (allNames) { + allNames.addAll(localNames); + } + } finally { + latch.countDown(); + } + }); + } + + latch.await(10, TimeUnit.SECONDS); + executor.shutdown(); + + // All names should be unique across all threads + assertThat(allNames).hasSize(threadCount * namesPerThread); + } + + // ==================== Member Statement Tests ==================== + + @Test + public void testAddReusableMember() { + context.addReusableMember("private int count;"); + context.addReusableMember("private String name;"); + + String code = context.reuseMemberCode(); + assertThat(code).contains("private int count;"); + assertThat(code).contains("private String name;"); + } + + @Test + public void testMemberStatementDeduplication() { + context.addReusableMember("private int count;"); + context.addReusableMember("private int count;"); + context.addReusableMember("private int count;"); + + String code = context.reuseMemberCode(); + // Should only appear once due to LinkedHashSet + // Count occurrences by finding all matches + int count = countOccurrences(code, "private int count;"); + assertThat(count).isEqualTo(1); + } + + @Test + public void testMemberStatementOrdering() { + context.addReusableMember("private int a;"); + context.addReusableMember("private int b;"); + context.addReusableMember("private int c;"); + + String code = context.reuseMemberCode(); + // LinkedHashSet preserves insertion order + int posA = code.indexOf("private int a;"); + int posB = code.indexOf("private int b;"); + int posC = code.indexOf("private int c;"); + + assertThat(posA).isLessThan(posB); + assertThat(posB).isLessThan(posC); + } + + // ==================== Init Statement Tests ==================== + + @Test + public void testAddReusableInitStatement() { + context.addReusableInitStatement("this.count = 0;"); + context.addReusableInitStatement("this.name = \"default\";"); + + String code = context.reuseInitCode(); + assertThat(code).contains("this.count = 0;"); + assertThat(code).contains("this.name = \"default\";"); + } + + @Test + public void testInitStatementDeduplication() { + context.addReusableInitStatement("this.count = 0;"); + context.addReusableInitStatement("this.count = 0;"); + + String code = context.reuseInitCode(); + int count = countOccurrences(code, "this.count = 0;"); + assertThat(count).isEqualTo(1); + } + + // ==================== Reusable Object Tests ==================== + + @Test + public void testAddReusableObject() { + TestSerializable obj = new TestSerializable("test", 42); + String fieldTerm = context.addReusableObject(obj, "testObj", "TestSerializable"); + + assertThat(fieldTerm).startsWith("testObj$"); + + // Check member code + String memberCode = context.reuseMemberCode(); + assertThat(memberCode).contains("private transient TestSerializable " + fieldTerm + ";"); + + // Check init code + String initCode = context.reuseInitCode(); + assertThat(initCode).contains(fieldTerm + " = ((TestSerializable) references[0]);"); + + // Check references + Object[] refs = context.getReferences(); + assertThat(refs).hasSize(1); + assertThat(refs[0]).isInstanceOf(TestSerializable.class); + TestSerializable refObj = (TestSerializable) refs[0]; + assertThat(refObj.name).isEqualTo("test"); + assertThat(refObj.value).isEqualTo(42); + } + + @Test + public void testAddMultipleReusableObjects() { + TestSerializable obj1 = new TestSerializable("first", 1); + TestSerializable obj2 = new TestSerializable("second", 2); + + String field1 = context.addReusableObject(obj1, "obj", "TestSerializable"); + String field2 = context.addReusableObject(obj2, "obj", "TestSerializable"); + + Object[] refs = context.getReferences(); + assertThat(refs).hasSize(2); + + String initCode = context.reuseInitCode(); + assertThat(initCode).contains(field1 + " = ((TestSerializable) references[0]);"); + assertThat(initCode).contains(field2 + " = ((TestSerializable) references[1]);"); + } + + @Test + public void testReusableObjectDeepCopy() { + TestSerializable original = new TestSerializable("original", 100); + context.addReusableObject(original, "obj", "TestSerializable"); + + // Modify original + original.name = "modified"; + original.value = 999; + + // Reference should still have original values (deep copy) + Object[] refs = context.getReferences(); + TestSerializable refObj = (TestSerializable) refs[0]; + assertThat(refObj.name).isEqualTo("original"); + assertThat(refObj.value).isEqualTo(100); + } + + // ==================== References Tests ==================== + + @Test + public void testGetReferences() { + // Empty context returns empty array + assertThat(context.getReferences()).isEmpty(); + + // Add object and verify + TestSerializable obj = new TestSerializable("test", 1); + context.addReusableObject(obj, "obj", "TestSerializable"); + + Object[] refs1 = context.getReferences(); + Object[] refs2 = context.getReferences(); + + // Should return new array each time but with same content + assertThat(refs1).isNotSameAs(refs2); + assertThat(refs1).containsExactly(refs2); + } + + // ==================== Integration Tests ==================== + + @Test + public void testCompleteCodeGeneration() { + // Simulate a complete code generation scenario + TestSerializable comparator = new TestSerializable("comparator", 1); + String comparatorField = + context.addReusableObject(comparator, "comparator", "TestSerializable"); + + context.addReusableMember("private int cachedHash;"); + + // Verify all code sections + String memberCode = context.reuseMemberCode(); + assertThat(memberCode).contains("private transient TestSerializable " + comparatorField); + assertThat(memberCode).contains("private int cachedHash;"); + + String initCode = context.reuseInitCode(); + assertThat(initCode).contains(comparatorField + " = ((TestSerializable) references[0]);"); + + Object[] refs = context.getReferences(); + assertThat(refs).hasSize(1); + } + + @Test + public void testEmptyContext() { + // Fresh context should return empty strings/arrays + assertThat(context.reuseMemberCode()).isEmpty(); + assertThat(context.reuseInitCode()).isEmpty(); + assertThat(context.getReferences()).isEmpty(); + } + + // ==================== Helper Classes ==================== + + /** Test serializable class for object reference tests. */ + private static class TestSerializable implements Serializable { + private static final long serialVersionUID = 1L; + String name; + int value; + + TestSerializable(String name, int value) { + this.name = name; + this.value = value; + } + } + + // ==================== Helper Methods ==================== + + /** Counts the number of occurrences of a substring in a string. */ + private static int countOccurrences(String str, String sub) { + int count = 0; + int idx = 0; + while ((idx = str.indexOf(sub, idx)) != -1) { + count++; + idx += sub.length(); + } + return count; + } +} diff --git a/fluss-codegen/src/test/java/org/apache/fluss/codegen/CompileUtilsTest.java b/fluss-codegen/src/test/java/org/apache/fluss/codegen/CompileUtilsTest.java new file mode 100644 index 0000000000..66c754759a --- /dev/null +++ b/fluss-codegen/src/test/java/org/apache/fluss/codegen/CompileUtilsTest.java @@ -0,0 +1,397 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.junit.jupiter.api.Test; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link CompileUtils}. + * + *

Test coverage includes: + * + *

    + *
  • Successful compilation of valid code + *
  • Compilation caching behavior + *
  • Cache clearing + *
  • Invalid code handling (syntax errors) + *
  • Class not found handling + *
  • Null parameter handling + *
  • Thread safety of compilation + *
  • Different class loaders produce different cache entries + *
+ */ +public class CompileUtilsTest { + + private static final AtomicInteger CLASS_COUNTER = new AtomicInteger(0); + + // Note: We don't clear the cache in @BeforeEach/@AfterEach because: + // 1. Tests use unique class names via uniqueClassName() + // 2. Clearing cache can interfere with parallel test execution + // 3. The cache is designed to be persistent for performance + + // ==================== Successful Compilation Tests ==================== + + @Test + public void testCompileSimpleClass() { + String className = uniqueClassName("SimpleClass"); + String code = + "public class " + + className + + " {\n" + + " public String hello() { return \"Hello\"; }\n" + + "}"; + + Class clazz = + CompileUtils.compile( + Thread.currentThread().getContextClassLoader(), className, code); + + assertThat(clazz).isNotNull(); + assertThat(clazz.getName()).isEqualTo(className); + } + + @Test + public void testCompileClassWithConstructor() throws Exception { + String className = uniqueClassName("WithConstructor"); + String code = + "public class " + + className + + " {\n" + + " private int value;\n" + + " public " + + className + + "(Object[] refs) {\n" + + " this.value = 42;\n" + + " }\n" + + " public int getValue() { return value; }\n" + + "}"; + + Class clazz = + CompileUtils.compile( + Thread.currentThread().getContextClassLoader(), className, code); + + Object instance = + clazz.getConstructor(Object[].class).newInstance(new Object[] {new Object[0]}); + int value = (int) clazz.getMethod("getValue").invoke(instance); + assertThat(value).isEqualTo(42); + } + + @Test + public void testCompileClassImplementingInterface() throws Exception { + String className = uniqueClassName("RunnableImpl"); + String code = + "public class " + + className + + " implements Runnable {\n" + + " private boolean ran = false;\n" + + " public void run() { ran = true; }\n" + + " public boolean hasRun() { return ran; }\n" + + "}"; + + Class clazz = + CompileUtils.compile( + Thread.currentThread().getContextClassLoader(), className, code); + + assertThat(Runnable.class.isAssignableFrom(clazz)).isTrue(); + + Runnable instance = (Runnable) clazz.getDeclaredConstructor().newInstance(); + instance.run(); + boolean hasRun = (boolean) clazz.getMethod("hasRun").invoke(instance); + assertThat(hasRun).isTrue(); + } + + // ==================== Caching Tests ==================== + + @Test + public void testCompilationCaching() { + String className = uniqueClassName("CachedClass"); + String code = "public class " + className + " {}"; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + Class class1 = CompileUtils.compile(classLoader, className, code); + Class class2 = CompileUtils.compile(classLoader, className, code); + + // Should return the same class instance from cache + assertThat(class1).isSameAs(class2); + } + + @Test + public void testDifferentCodeProducesDifferentClasses() { + String className1 = uniqueClassName("Class"); + String className2 = uniqueClassName("Class"); + String code1 = "public class " + className1 + " { public int value() { return 1; } }"; + String code2 = "public class " + className2 + " { public int value() { return 2; } }"; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + Class class1 = CompileUtils.compile(classLoader, className1, code1); + Class class2 = CompileUtils.compile(classLoader, className2, code2); + + assertThat(class1).isNotSameAs(class2); + assertThat(class1.getName()).isNotEqualTo(class2.getName()); + } + + @Test + public void testDifferentClassLoadersProduceDifferentCacheEntries() { + String className = uniqueClassName("MultiLoaderClass"); + String code = "public class " + className + " {}"; + + // Create two different class loaders + ClassLoader loader1 = + new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()); + ClassLoader loader2 = + new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()); + + // Different class loader hash codes should produce different cache entries + // Note: This tests the cache key mechanism, not that the classes are different + Class class1 = CompileUtils.compile(loader1, className, code); + Class class2 = CompileUtils.compile(loader2, className, code); + + // Both should compile successfully + assertThat(class1).isNotNull(); + assertThat(class2).isNotNull(); + assertThat(class1.getName()).isEqualTo(className); + assertThat(class2.getName()).isEqualTo(className); + } + + @Test + public void testClearCache() { + String className = uniqueClassName("ClearCacheClass"); + String code = "public class " + className + " {}"; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + Class class1 = CompileUtils.compile(classLoader, className, code); + + CompileUtils.clearCache(); + + // After clearing, should compile again (new class instance) + Class class2 = CompileUtils.compile(classLoader, className, code); + + // The classes should be equal (same name) but may or may not be same instance + // depending on JVM class loading behavior + assertThat(class1.getName()).isEqualTo(class2.getName()); + } + + // ==================== Error Handling Tests ==================== + + @Test + public void testCompileInvalidCode() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + // Syntax error + assertThatThrownBy( + () -> + CompileUtils.compile( + classLoader, + "SyntaxErrorClass", + "public class SyntaxErrorClass { invalid syntax }")) + .isInstanceOf(CodeGenException.class) + .hasMessageContaining("Code generation cannot be compiled"); + + // Missing braces + assertThatThrownBy( + () -> + CompileUtils.compile( + classLoader, + "MissingBraceClass", + "public class MissingBraceClass { public void test() { int x = 1;")) + .isInstanceOf(CodeGenException.class); + + // Wrong class name + assertThatThrownBy( + () -> + CompileUtils.compile( + classLoader, + "WrongClassName", + "public class ActualClassName {}")) + .isInstanceOf(CodeGenException.class) + .hasMessageContaining("Cannot load class"); + } + + @Test + public void testCompileNullParametersThrow() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + assertThatThrownBy(() -> CompileUtils.compile(null, "TestClass", "code")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("classLoader must not be null"); + + assertThatThrownBy(() -> CompileUtils.compile(classLoader, null, "code")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("name must not be null"); + + assertThatThrownBy(() -> CompileUtils.compile(classLoader, "TestClass", null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("code must not be null"); + } + + // ==================== Thread Safety Tests ==================== + + @Test + public void testConcurrentCompilationSameCode() throws InterruptedException { + String className = uniqueClassName("ConcurrentClass"); + String code = "public class " + className + " {}"; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + int threadCount = 10; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threadCount); + AtomicReference> firstClass = new AtomicReference<>(); + AtomicInteger successCount = new AtomicInteger(0); + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + for (int i = 0; i < threadCount; i++) { + executor.submit( + () -> { + try { + startLatch.await(); + Class clazz = CompileUtils.compile(classLoader, className, code); + firstClass.compareAndSet(null, clazz); + // All threads should get the same class instance due to caching + if (clazz == firstClass.get()) { + successCount.incrementAndGet(); + } + } catch (Exception e) { + // Ignore + } finally { + doneLatch.countDown(); + } + }); + } + + startLatch.countDown(); + doneLatch.await(30, TimeUnit.SECONDS); + executor.shutdown(); + + // All threads should have gotten the same cached class + assertThat(successCount.get()).isEqualTo(threadCount); + } + + @Test + public void testConcurrentCompilationDifferentCode() throws InterruptedException { + int threadCount = 5; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threadCount); + AtomicInteger successCount = new AtomicInteger(0); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + for (int i = 0; i < threadCount; i++) { + final int index = i; + executor.submit( + () -> { + try { + startLatch.await(); + String className = uniqueClassName("ConcurrentDiff" + index); + String code = + "public class " + + className + + " { public int id() { return " + + index + + "; } }"; + Class clazz = CompileUtils.compile(classLoader, className, code); + if (clazz != null && clazz.getName().equals(className)) { + successCount.incrementAndGet(); + } + } catch (Exception e) { + // Ignore + } finally { + doneLatch.countDown(); + } + }); + } + + startLatch.countDown(); + doneLatch.await(30, TimeUnit.SECONDS); + executor.shutdown(); + + assertThat(successCount.get()).isEqualTo(threadCount); + } + + // ==================== Complex Code Tests ==================== + + @Test + public void testCompileComplexClass() throws Exception { + String className = uniqueClassName("ComplexClass"); + String code = + "import java.util.ArrayList;\n" + + "import java.util.List;\n" + + "public class " + + className + + " {\n" + + " private List items = new ArrayList();\n" + + " public void add(Object item) { items.add(item); }\n" + + " public int size() { return items.size(); }\n" + + " public Object get(int index) { return items.get(index); }\n" + + "}"; + + Class clazz = + CompileUtils.compile( + Thread.currentThread().getContextClassLoader(), className, code); + + Object instance = clazz.getDeclaredConstructor().newInstance(); + clazz.getMethod("add", Object.class).invoke(instance, "hello"); + clazz.getMethod("add", Object.class).invoke(instance, "world"); + + int size = (int) clazz.getMethod("size").invoke(instance); + assertThat(size).isEqualTo(2); + + String first = (String) clazz.getMethod("get", int.class).invoke(instance, 0); + assertThat(first).isEqualTo("hello"); + } + + @Test + public void testCompileWithInnerClass() throws Exception { + String className = uniqueClassName("OuterClass"); + String code = + "public class " + + className + + " {\n" + + " private Inner inner = new Inner();\n" + + " public int getValue() { return inner.value; }\n" + + " private class Inner {\n" + + " int value = 99;\n" + + " }\n" + + "}"; + + Class clazz = + CompileUtils.compile( + Thread.currentThread().getContextClassLoader(), className, code); + + Object instance = clazz.getDeclaredConstructor().newInstance(); + int value = (int) clazz.getMethod("getValue").invoke(instance); + assertThat(value).isEqualTo(99); + } + + // ==================== Helper Methods ==================== + + private static String uniqueClassName(String prefix) { + return prefix + "_" + CLASS_COUNTER.incrementAndGet(); + } +} diff --git a/fluss-codegen/src/test/java/org/apache/fluss/codegen/GeneratedClassTest.java b/fluss-codegen/src/test/java/org/apache/fluss/codegen/GeneratedClassTest.java new file mode 100644 index 0000000000..3196d83f51 --- /dev/null +++ b/fluss-codegen/src/test/java/org/apache/fluss/codegen/GeneratedClassTest.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.apache.fluss.utils.InstantiationUtils; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link GeneratedClass}. + * + *

Test coverage includes: + * + *

    + *
  • Instance creation with and without references + *
  • Compilation caching within GeneratedClass + *
  • Serialization and deserialization + *
  • Error handling for invalid code + *
  • Null parameter handling + *
  • Getter methods + *
  • Reference passing to constructor + *
+ */ +public class GeneratedClassTest { + + private static final AtomicInteger CLASS_COUNTER = new AtomicInteger(0); + + // Note: We don't clear the cache in @BeforeEach/@AfterEach because: + // 1. Tests use unique class names via uniqueClassName() + // 2. Clearing cache can interfere with parallel test execution + // 3. The cache is designed to be persistent for performance // ==================== Basic + // Instantiation Tests ==================== + + @Test + public void testNewInstanceSimple() { + String className = uniqueClassName("SimpleGenerated"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {}\n" + + " public String hello() { return \"Hello\"; }\n" + + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + Object instance = generated.newInstance(Thread.currentThread().getContextClassLoader()); + + assertThat(instance).isNotNull(); + assertThat(instance.getClass().getName()).isEqualTo(className); + } + + @Test + public void testNewInstanceWithReferences() throws Exception { + String className = uniqueClassName("WithRefs"); + String code = + "public class " + + className + + " {\n" + + " private String message;\n" + + " private int number;\n" + + " public " + + className + + "(Object[] refs) {\n" + + " this.message = (String) refs[0];\n" + + " this.number = (Integer) refs[1];\n" + + " }\n" + + " public String getMessage() { return message; }\n" + + " public int getNumber() { return number; }\n" + + "}"; + + Object[] refs = new Object[] {"Hello World", 42}; + GeneratedClass generated = new GeneratedClass(className, code, refs); + Object instance = generated.newInstance(Thread.currentThread().getContextClassLoader()); + + String message = (String) instance.getClass().getMethod("getMessage").invoke(instance); + int number = (int) instance.getClass().getMethod("getNumber").invoke(instance); + + assertThat(message).isEqualTo("Hello World"); + assertThat(number).isEqualTo(42); + } + + @Test + public void testNewInstanceWithEmptyReferences() { + String className = uniqueClassName("EmptyRefs"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {\n" + + " if (refs.length != 0) throw new RuntimeException(\"Expected empty refs\");\n" + + " }\n" + + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + Object instance = generated.newInstance(Thread.currentThread().getContextClassLoader()); + + assertThat(instance).isNotNull(); + } + + // ==================== Compilation Caching Tests ==================== + + @Test + public void testCompileCachesClass() { + String className = uniqueClassName("CachedCompile"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {}\n" + + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + Class class1 = generated.compile(classLoader); + Class class2 = generated.compile(classLoader); + + // Should return the same cached class + assertThat(class1).isSameAs(class2); + } + + @Test + public void testMultipleNewInstancesUseSameClass() { + String className = uniqueClassName("MultiInstance"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {}\n" + + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + Object instance1 = generated.newInstance(classLoader); + Object instance2 = generated.newInstance(classLoader); + + // Different instances but same class + assertThat(instance1).isNotSameAs(instance2); + assertThat(instance1.getClass()).isSameAs(instance2.getClass()); + } + + // ==================== Serialization Tests ==================== + + @Test + public void testSerializationRoundTrip() throws Exception { + String className = uniqueClassName("Serializable"); + String code = + "public class " + + className + + " implements java.io.Serializable {\n" + + " private static final long serialVersionUID = 1L;\n" + + " private String value;\n" + + " public " + + className + + "(Object[] refs) {\n" + + " this.value = (String) refs[0];\n" + + " }\n" + + " public String getValue() { return value; }\n" + + "}"; + + Object[] refs = new Object[] {"test-value"}; + GeneratedClass original = new GeneratedClass(className, code, refs); + + // Serialize and deserialize + byte[] serialized = InstantiationUtils.serializeObject(original); + GeneratedClass deserialized = + InstantiationUtils.deserializeObject( + serialized, Thread.currentThread().getContextClassLoader()); + + // Verify deserialized GeneratedClass works + assertThat(deserialized.getClassName()).isEqualTo(className); + assertThat(deserialized.getCode()).isEqualTo(code); + assertThat(deserialized.getReferences()).containsExactly(refs); + + // Create instance from deserialized + Object instance = deserialized.newInstance(Thread.currentThread().getContextClassLoader()); + String value = (String) instance.getClass().getMethod("getValue").invoke(instance); + assertThat(value).isEqualTo("test-value"); + } + + @Test + public void testSerializationClearsCompiledClass() throws Exception { + String className = uniqueClassName("SerialClear"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {}\n" + + "}"; + + GeneratedClass original = new GeneratedClass(className, code); + + // Compile the class first + original.compile(Thread.currentThread().getContextClassLoader()); + + // Serialize and deserialize + byte[] serialized = InstantiationUtils.serializeObject(original); + GeneratedClass deserialized = + InstantiationUtils.deserializeObject( + serialized, Thread.currentThread().getContextClassLoader()); + + // The deserialized instance should still work (compiledClass is transient) + Object instance = deserialized.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat(instance).isNotNull(); + } + + // ==================== Getter Tests ==================== + + @Test + public void testGetters() { + String className = uniqueClassName("TestGetters"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {}\n" + + "}"; + Object[] refs = new Object[] {"a", 1, true}; + + // Test with references + GeneratedClass withRefs = new GeneratedClass(className, code, refs); + assertThat(withRefs.getClassName()).isEqualTo(className); + assertThat(withRefs.getCode()).isEqualTo(code); + assertThat(withRefs.getReferences()).containsExactly("a", 1, true); + + // Test without references + String className2 = uniqueClassName("TestGetters"); + String code2 = + "public class " + className2 + " { public " + className2 + "(Object[] refs) {} }"; + GeneratedClass noRefs = new GeneratedClass(className2, code2); + assertThat(noRefs.getReferences()).isEmpty(); + } + + // ==================== Error Handling Tests ==================== + + @Test + public void testNewInstanceWithInvalidCode() { + String className = "InvalidCode"; + String code = "public class " + className + " { invalid syntax }"; + + GeneratedClass generated = new GeneratedClass(className, code); + + assertThatThrownBy( + () -> generated.newInstance(Thread.currentThread().getContextClassLoader())) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Could not instantiate generated class"); + } + + @Test + public void testNewInstanceWithMissingConstructor() { + String className = uniqueClassName("NoConstructor"); + // Class without Object[] constructor + String code = + "public class " + className + " {\n" + " public " + className + "() {}\n" + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + + assertThatThrownBy( + () -> generated.newInstance(Thread.currentThread().getContextClassLoader())) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Could not instantiate generated class"); + } + + @Test + public void testNewInstanceWithConstructorException() { + String className = uniqueClassName("ThrowingConstructor"); + String code = + "public class " + + className + + " {\n" + + " public " + + className + + "(Object[] refs) {\n" + + " throw new RuntimeException(\"Constructor failed\");\n" + + " }\n" + + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + + assertThatThrownBy( + () -> generated.newInstance(Thread.currentThread().getContextClassLoader())) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Could not instantiate generated class"); + } + + @Test + public void testNullParametersThrow() { + assertThatThrownBy(() -> new GeneratedClass(null, "code")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("className must not be null"); + + assertThatThrownBy(() -> new GeneratedClass("ClassName", null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("code must not be null"); + + assertThatThrownBy(() -> new GeneratedClass("ClassName", "code", null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("references must not be null"); + } + + // ==================== Interface Implementation Tests ==================== + + @Test + public void testGeneratedClassImplementsInterface() { + String className = uniqueClassName("RunnableImpl"); + String code = + "public class " + + className + + " implements Runnable {\n" + + " private boolean executed = false;\n" + + " public " + + className + + "(Object[] refs) {}\n" + + " public void run() { executed = true; }\n" + + " public boolean isExecuted() { return executed; }\n" + + "}"; + + GeneratedClass generated = new GeneratedClass(className, code); + Runnable instance = generated.newInstance(Thread.currentThread().getContextClassLoader()); + + assertThat(instance).isInstanceOf(Runnable.class); + instance.run(); + } + + // ==================== Complex Reference Tests ==================== + + @Test + public void testComplexReferences() throws Exception { + String className = uniqueClassName("ComplexRefs"); + String code = + "import java.util.List;\n" + + "import java.util.Map;\n" + + "public class " + + className + + " {\n" + + " private List list;\n" + + " private Map map;\n" + + " public " + + className + + "(Object[] refs) {\n" + + " this.list = (List) refs[0];\n" + + " this.map = (Map) refs[1];\n" + + " }\n" + + " public int getListSize() { return list.size(); }\n" + + " public int getMapSize() { return map.size(); }\n" + + "}"; + + java.util.List list = java.util.Arrays.asList("a", "b", "c"); + java.util.Map map = new java.util.HashMap(); + map.put("x", 1); + map.put("y", 2); + + Object[] refs = new Object[] {list, map}; + GeneratedClass generated = new GeneratedClass(className, code, refs); + Object instance = generated.newInstance(Thread.currentThread().getContextClassLoader()); + + int listSize = (int) instance.getClass().getMethod("getListSize").invoke(instance); + int mapSize = (int) instance.getClass().getMethod("getMapSize").invoke(instance); + + assertThat(listSize).isEqualTo(3); + assertThat(mapSize).isEqualTo(2); + } + + // ==================== Helper Methods ==================== + + private static String uniqueClassName(String prefix) { + return prefix + "_" + CLASS_COUNTER.incrementAndGet(); + } +} diff --git a/fluss-codegen/src/test/java/org/apache/fluss/codegen/JavaCodeBuilderTest.java b/fluss-codegen/src/test/java/org/apache/fluss/codegen/JavaCodeBuilderTest.java new file mode 100644 index 0000000000..a0d962ce9a --- /dev/null +++ b/fluss-codegen/src/test/java/org/apache/fluss/codegen/JavaCodeBuilderTest.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen; + +import org.apache.fluss.codegen.JavaCodeBuilder.Modifier; +import org.apache.fluss.codegen.JavaCodeBuilder.Param; +import org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; + +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.FINAL; +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.PRIVATE; +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.PUBLIC; +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.STATIC; +import static org.apache.fluss.codegen.JavaCodeBuilder.Modifier.TRANSIENT; +import static org.apache.fluss.codegen.JavaCodeBuilder.Param.of; +import static org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType.BOOLEAN; +import static org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType.INT; +import static org.apache.fluss.codegen.JavaCodeBuilder.PrimitiveType.VOID; +import static org.apache.fluss.codegen.JavaCodeBuilder.arrayOf; +import static org.apache.fluss.codegen.JavaCodeBuilder.mods; +import static org.apache.fluss.codegen.JavaCodeBuilder.params; +import static org.apache.fluss.codegen.JavaCodeBuilder.typeOf; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JavaCodeBuilder}. + * + *

Test strategy: + * + *

    + *
  • Code generation tests use expected file assertions for precise validation + *
  • Helper method tests verify static utilities with exact equality + *
  • Edge case tests verify specific boundary behaviors + *
+ * + *

Expected files: {@code src/test/resources/expected/java-code-builder/} + * + *

Update expected files: {@code mvn test -Dcodegen.update.expected=true} + */ +public class JavaCodeBuilderTest { + + private static final String EXPECTED_DIR = "java-code-builder"; + + private JavaCodeBuilder builder; + + @BeforeEach + public void setUp() { + builder = new JavaCodeBuilder(); + } + + // ==================== Code Generation Tests ==================== + + /** Tests basic class structure: empty class, interface implementation. */ + @Test + public void testClassStructure() { + String code = + builder.beginClass(new Modifier[] {PUBLIC, FINAL}, "MyClass", "Serializable") + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected( + code, EXPECTED_DIR + "/classStructure.java.expected"); + } + + /** Tests field declarations: primitive, object, with initialization. */ + @Test + public void testFields() { + String code = + builder.beginClass(PUBLIC, "TestClass", null) + .field(PRIVATE, INT, "count") + .field(new Modifier[] {PRIVATE, FINAL}, "String", "name") + .fieldWithInit(PRIVATE, BOOLEAN, "active", "true") + .fieldWithInit(new Modifier[] {PRIVATE, STATIC, FINAL}, INT, "MAX", "100") + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/fields.java.expected"); + } + + /** Tests constructor with parameters. */ + @Test + public void testConstructor() { + String code = + builder.beginClass(PUBLIC, "TestClass", null) + .beginConstructor( + PUBLIC, "TestClass", of(INT, "value"), of("String", "name")) + .stmt("this.value = value") + .stmt("this.name = name") + .endConstructor() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/constructor.java.expected"); + } + + /** Tests method with @Override annotation. */ + @Test + public void testMethodWithOverride() { + String code = + builder.beginClass(PUBLIC, "TestClass", null) + .override() + .beginMethod(PUBLIC, "String", "toString") + .returnStmt("\"TestClass\"") + .endMethod() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected( + code, EXPECTED_DIR + "/methodWithOverride.java.expected"); + } + + /** Tests if/else-if/else control flow with proper indentation. */ + @Test + public void testIfElseIfElse() { + String code = + builder.beginClass(PUBLIC, "TestClass", null) + .beginMethod(PUBLIC, "String", "classify", of(INT, "x")) + .beginIf("x > 0") + .returnStmt("\"positive\"") + .beginElseIf("x < 0") + .returnStmt("\"negative\"") + .beginElse() + .returnStmt("\"zero\"") + .endIf() + .endMethod() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/ifElseIfElse.java.expected"); + } + + /** Tests nested control flow: for loops with if/else, break/continue. */ + @Test + public void testNestedControlFlow() { + String code = + builder.beginClass(PUBLIC, "TestClass", null) + .beginMethod(PUBLIC, VOID, "process", of("int[][]", "matrix")) + .beginFor("int i = 0", "i < matrix.length", "i++") + .beginFor("int j = 0", "j < matrix[i].length", "j++") + .beginIf("matrix[i][j] < 0") + .continueStmt() + .beginElseIf("matrix[i][j] == 0") + .breakStmt() + .beginElse() + .stmt("System.out.println(matrix[i][j])") + .endIf() + .endFor() + .endFor() + .endMethod() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected( + code, EXPECTED_DIR + "/nestedControlFlow.java.expected"); + } + + /** Tests raw code insertion with and without indentation. */ + @Test + public void testRawCode() { + String code = + builder.beginClass(PUBLIC, "TestClass", null) + .raw("// Class-level comment") + .raw("private static final int CONST = 42;") + .newLine() + .rawUnindented("// Unindented comment at class level") + .beginMethod(PUBLIC, VOID, "test") + .raw("// Multi-line raw\nint x = 1;\nint y = 2;") + .endMethod() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/rawCode.java.expected"); + } + + /** Tests complete class with all features: fields, constructor, methods, control flow. */ + @Test + public void testCompleteClass() { + String code = + builder.beginClass(new Modifier[] {PUBLIC, FINAL}, "RecordEqualiser", "Equaliser") + .field(PRIVATE, INT, "fieldCount") + .field(new Modifier[] {PRIVATE, FINAL}, "Object[]", "references") + .newLine() + .beginConstructor(PUBLIC, "RecordEqualiser", of("Object[]", "references")) + .stmt("this.references = references") + .stmt("this.fieldCount = 0") + .endConstructor() + .newLine() + .override() + .beginMethod( + PUBLIC, + BOOLEAN, + "equals", + of("Object", "left"), + of("Object", "right")) + .beginIf("left == null && right == null") + .returnStmt("true") + .endIf() + .beginIf("left == null || right == null") + .returnStmt("false") + .endIf() + .declare(BOOLEAN, "result", "true") + .beginFor("int i = 0", "i < fieldCount", "i++") + .stmt("result = result && compareField(i, left, right)") + .endFor() + .returnStmt("result") + .endMethod() + .newLine() + .beginMethod( + PRIVATE, + BOOLEAN, + "compareField", + of(INT, "idx"), + of("Object", "l"), + of("Object", "r")) + .returnStmt("l.equals(r)") + .endMethod() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/completeClass.java.expected"); + } + + /** Tests type-safe API with full type references. */ + @Test + public void testTypeSafeApi() { + String code = + builder.beginClass( + new Modifier[] {PUBLIC, FINAL}, + "Calculator", + typeOf(Serializable.class)) + .field(new Modifier[] {PRIVATE}, INT, "result") + .newLine() + .beginConstructor(PUBLIC, "Calculator", of(arrayOf(Object.class), "refs")) + .stmt("this.result = 0") + .endConstructor() + .newLine() + .beginMethod(PUBLIC, INT, "add", of(INT, "a"), of(INT, "b")) + .declare(INT, "sum", "a + b") + .returnStmt("sum") + .endMethod() + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/typeSafeApi.java.expected"); + } + + // ==================== Helper Method Tests ==================== + + @Test + public void testModsHelper() { + assertThat(mods(PUBLIC)).isEqualTo("public"); + assertThat(mods(PUBLIC, FINAL)).isEqualTo("public final"); + assertThat(mods(PRIVATE, STATIC, FINAL)).isEqualTo("private static final"); + assertThat(mods(PRIVATE, TRANSIENT)).isEqualTo("private transient"); + } + + @Test + public void testParamsHelper() { + assertThat(params()).isEqualTo(""); + assertThat(params(of(INT, "a"))).isEqualTo("int a"); + assertThat(params(of(INT, "a"), of(INT, "b"))).isEqualTo("int a, int b"); + assertThat(params(of(String.class, "name"), of(BOOLEAN, "flag"))) + .isEqualTo("java.lang.String name, boolean flag"); + } + + @Test + public void testTypeOfHelper() { + assertThat(typeOf(String.class)).isEqualTo("java.lang.String"); + assertThat(typeOf(Serializable.class)).isEqualTo("java.io.Serializable"); + } + + @Test + public void testArrayOfHelper() { + assertThat(arrayOf(INT)).isEqualTo("int[]"); + assertThat(arrayOf(String.class)).isEqualTo("java.lang.String[]"); + assertThat(arrayOf("Object")).isEqualTo("Object[]"); + } + + @Test + public void testParamClass() { + Param p1 = of(INT, "count"); + assertThat(p1.getType()).isEqualTo("int"); + assertThat(p1.getName()).isEqualTo("count"); + assertThat(p1.toString()).isEqualTo("int count"); + + Param p2 = of(String.class, "name"); + assertThat(p2.toString()).isEqualTo("java.lang.String name"); + + Param p3 = of("InternalRow", "row"); + assertThat(p3.toString()).isEqualTo("InternalRow row"); + } + + @Test + public void testModifierEnum() { + assertThat(Modifier.PUBLIC.toString()).isEqualTo("public"); + assertThat(Modifier.PRIVATE.toString()).isEqualTo("private"); + assertThat(Modifier.STATIC.toString()).isEqualTo("static"); + assertThat(Modifier.FINAL.toString()).isEqualTo("final"); + } + + @Test + public void testPrimitiveTypeEnum() { + assertThat(PrimitiveType.BOOLEAN.toString()).isEqualTo("boolean"); + assertThat(PrimitiveType.INT.toString()).isEqualTo("int"); + assertThat(PrimitiveType.LONG.toString()).isEqualTo("long"); + assertThat(PrimitiveType.VOID.toString()).isEqualTo("void"); + } + + // ==================== Edge Case Tests ==================== + + /** Tests that empty/null/whitespace raw code is handled correctly. */ + @Test + public void testEmptyRawCode() { + String code = + builder.beginClass(PUBLIC, "Test", null) + .raw("") + .raw(null) + .raw(" \n \n ") + .rawUnindented("") + .rawUnindented(null) + .field(PRIVATE, INT, "x") + .endClass() + .build(); + + CodeGenTestUtils.assertMatchesExpected(code, EXPECTED_DIR + "/emptyRawCode.java.expected"); + } + + /** Tests toString() returns same as build(). */ + @Test + public void testToString() { + builder.beginClass(PUBLIC, "Test", null).field(PRIVATE, INT, "x").endClass(); + assertThat(builder.toString()).isEqualTo(builder.build()); + } +} diff --git a/fluss-codegen/src/test/java/org/apache/fluss/codegen/generator/EqualiserCodeGeneratorTest.java b/fluss-codegen/src/test/java/org/apache/fluss/codegen/generator/EqualiserCodeGeneratorTest.java new file mode 100644 index 0000000000..465956492a --- /dev/null +++ b/fluss-codegen/src/test/java/org/apache/fluss/codegen/generator/EqualiserCodeGeneratorTest.java @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fluss.codegen.generator; + +import org.apache.fluss.codegen.CodeGenTestUtils; +import org.apache.fluss.codegen.GeneratedClass; +import org.apache.fluss.codegen.types.RecordEqualiser; +import org.apache.fluss.row.BinaryString; +import org.apache.fluss.row.Decimal; +import org.apache.fluss.row.GenericArray; +import org.apache.fluss.row.GenericMap; +import org.apache.fluss.row.GenericRow; +import org.apache.fluss.row.TimestampLtz; +import org.apache.fluss.row.TimestampNtz; +import org.apache.fluss.types.DataType; +import org.apache.fluss.types.DataTypes; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EqualiserCodeGenerator}. + * + *

Each test verifies both: + * + *

    + *
  • Generated code structure via expected file assertion + *
  • Runtime behavior via actual comparison + *
+ */ +public class EqualiserCodeGeneratorTest { + + private static final String EXPECTED_DIR = "equaliser-code-generator/"; + + // ==================== Primitive Types ==================== + + @Test + public void testIntField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.INT()}) + .generateRecordEqualiser("IntFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testIntField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat(equaliser.equals(GenericRow.of(1), GenericRow.of(1))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(1), GenericRow.of(2))).isFalse(); + } + + @Test + public void testBooleanField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.BOOLEAN()}) + .generateRecordEqualiser("BooleanFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testBooleanField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat(equaliser.equals(GenericRow.of(true), GenericRow.of(true))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(true), GenericRow.of(false))).isFalse(); + } + + @Test + public void testLongField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.BIGINT()}) + .generateRecordEqualiser("LongFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testLongField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat(equaliser.equals(GenericRow.of(100L), GenericRow.of(100L))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(100L), GenericRow.of(200L))).isFalse(); + } + + @Test + public void testDoubleField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.DOUBLE()}) + .generateRecordEqualiser("DoubleFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testDoubleField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat(equaliser.equals(GenericRow.of(1.5), GenericRow.of(1.5))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(1.5), GenericRow.of(2.5))).isFalse(); + } + + // ==================== String and Binary Types ==================== + + @Test + public void testStringField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.STRING()}) + .generateRecordEqualiser("StringFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testStringField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat( + equaliser.equals( + GenericRow.of(BinaryString.fromString("hello")), + GenericRow.of(BinaryString.fromString("hello")))) + .isTrue(); + assertThat( + equaliser.equals( + GenericRow.of(BinaryString.fromString("hello")), + GenericRow.of(BinaryString.fromString("world")))) + .isFalse(); + } + + @Test + public void testBinaryField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.BINARY(5)}) + .generateRecordEqualiser("BinaryFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testBinaryField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + byte[] b1 = new byte[] {1, 2, 3, 4, 5}; + byte[] b2 = new byte[] {1, 2, 3, 4, 5}; + byte[] b3 = new byte[] {1, 2, 3, 4, 6}; + assertThat(equaliser.equals(GenericRow.of((Object) b1), GenericRow.of((Object) b2))) + .isTrue(); + assertThat(equaliser.equals(GenericRow.of((Object) b1), GenericRow.of((Object) b3))) + .isFalse(); + } + + @Test + public void testBytesField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.BYTES()}) + .generateRecordEqualiser("BytesFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testBytesField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + assertThat( + equaliser.equals( + GenericRow.of((Object) new byte[] {1, 2}), + GenericRow.of((Object) new byte[] {1, 2}))) + .isTrue(); + assertThat( + equaliser.equals( + GenericRow.of((Object) new byte[] {1, 2}), + GenericRow.of((Object) new byte[] {1, 3}))) + .isFalse(); + } + + // ==================== Comparable Types ==================== + + @Test + public void testDecimalField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.DECIMAL(10, 2)}) + .generateRecordEqualiser("DecimalFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testDecimalField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + Decimal d1 = Decimal.fromUnscaledLong(12345, 10, 2); + Decimal d2 = Decimal.fromUnscaledLong(12345, 10, 2); + Decimal d3 = Decimal.fromUnscaledLong(12346, 10, 2); + assertThat(equaliser.equals(GenericRow.of(d1), GenericRow.of(d2))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(d1), GenericRow.of(d3))).isFalse(); + } + + @Test + public void testTimestampNtzField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.TIMESTAMP(6)}) + .generateRecordEqualiser("TimestampNtzFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testTimestampNtzField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + TimestampNtz ts1 = TimestampNtz.fromMillis(1684814400000L); + TimestampNtz ts2 = TimestampNtz.fromMillis(1684814400000L); + TimestampNtz ts3 = TimestampNtz.fromMillis(1684814500000L); + assertThat(equaliser.equals(GenericRow.of(ts1), GenericRow.of(ts2))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(ts1), GenericRow.of(ts3))).isFalse(); + } + + @Test + public void testTimestampLtzField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.TIMESTAMP_LTZ(3)}) + .generateRecordEqualiser("TimestampLtzFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testTimestampLtzField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + TimestampLtz ts1 = TimestampLtz.fromEpochMillis(1684814600000L); + TimestampLtz ts2 = TimestampLtz.fromEpochMillis(1684814600000L); + TimestampLtz ts3 = TimestampLtz.fromEpochMillis(1684814700000L); + assertThat(equaliser.equals(GenericRow.of(ts1), GenericRow.of(ts2))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(ts1), GenericRow.of(ts3))).isFalse(); + } + + // ==================== Composite Types ==================== + + @Test + public void testArrayField() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.ARRAY(DataTypes.INT())}) + .generateRecordEqualiser("ArrayFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testArrayField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + GenericArray arr1 = new GenericArray(new Integer[] {1, 2, 3}); + GenericArray arr2 = new GenericArray(new Integer[] {1, 2, 3}); + GenericArray arr3 = new GenericArray(new Integer[] {1, 2, 4}); + assertThat(equaliser.equals(GenericRow.of(arr1), GenericRow.of(arr2))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(arr1), GenericRow.of(arr3))).isFalse(); + } + + @Test + public void testMapField() { + GeneratedClass generated = + new EqualiserCodeGenerator( + new DataType[] {DataTypes.MAP(DataTypes.STRING(), DataTypes.INT())}) + .generateRecordEqualiser("MapFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testMapField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + + Map map1 = new HashMap<>(); + map1.put(BinaryString.fromString("a"), 1); + map1.put(BinaryString.fromString("b"), 2); + + Map map2 = new HashMap<>(); + map2.put(BinaryString.fromString("a"), 1); + map2.put(BinaryString.fromString("b"), 2); + + Map map3 = new HashMap<>(); + map3.put(BinaryString.fromString("a"), 1); + map3.put(BinaryString.fromString("b"), 3); + + assertThat( + equaliser.equals( + GenericRow.of(new GenericMap(map1)), + GenericRow.of(new GenericMap(map2)))) + .isTrue(); + assertThat( + equaliser.equals( + GenericRow.of(new GenericMap(map1)), + GenericRow.of(new GenericMap(map3)))) + .isFalse(); + } + + @Test + public void testNestedRowField() { + DataType nestedRowType = DataTypes.ROW(DataTypes.INT(), DataTypes.STRING()); + + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {nestedRowType}) + .generateRecordEqualiser("NestedRowFieldEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testNestedRowField.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + + GenericRow inner1 = GenericRow.of(1, BinaryString.fromString("test")); + GenericRow inner2 = GenericRow.of(1, BinaryString.fromString("test")); + GenericRow inner3 = GenericRow.of(1, BinaryString.fromString("other")); + + assertThat(equaliser.equals(GenericRow.of(inner1), GenericRow.of(inner2))).isTrue(); + assertThat(equaliser.equals(GenericRow.of(inner1), GenericRow.of(inner3))).isFalse(); + } + + // ==================== Multiple Fields ==================== + + @Test + public void testMultipleFields() { + DataType[] types = + new DataType[] { + DataTypes.INT(), + DataTypes.STRING(), + DataTypes.BOOLEAN(), + DataTypes.BIGINT(), + DataTypes.DOUBLE() + }; + + GeneratedClass generated = + new EqualiserCodeGenerator(types) + .generateRecordEqualiser("MultipleFieldsEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testMultipleFields.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + + GenericRow row1 = GenericRow.of(1, BinaryString.fromString("test"), true, 100L, 1.5); + GenericRow row2 = GenericRow.of(1, BinaryString.fromString("test"), true, 100L, 1.5); + GenericRow row3 = GenericRow.of(1, BinaryString.fromString("test"), false, 100L, 1.5); + + assertThat(equaliser.equals(row1, row2)).isTrue(); + assertThat(equaliser.equals(row1, row3)).isFalse(); + } + + // ==================== Projection ==================== + + @Test + public void testProjection() { + DataType[] types = new DataType[] {DataTypes.INT(), DataTypes.STRING(), DataTypes.BIGINT()}; + int[] projection = new int[] {1, 2}; // Only compare STRING and BIGINT + + GeneratedClass generated = + new EqualiserCodeGenerator(types, projection) + .generateRecordEqualiser("ProjectionEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testProjection.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + + // Different INT (field 0) but same STRING and BIGINT (fields 1, 2) + GenericRow row1 = GenericRow.of(1, BinaryString.fromString("test"), 100L); + GenericRow row2 = GenericRow.of(2, BinaryString.fromString("test"), 100L); + GenericRow row3 = GenericRow.of(1, BinaryString.fromString("other"), 100L); + + // Should be equal because INT is not in projection + assertThat(equaliser.equals(row1, row2)).isTrue(); + // Should not be equal because STRING differs + assertThat(equaliser.equals(row1, row3)).isFalse(); + } + + // ==================== Null Handling ==================== + + @Test + public void testNullHandling() { + GeneratedClass generated = + new EqualiserCodeGenerator(new DataType[] {DataTypes.INT()}) + .generateRecordEqualiser("NullHandlingEqualiser"); + + CodeGenTestUtils.assertMatchesExpectedNormalized( + generated.getCode(), EXPECTED_DIR + "testNullHandling.java.expected"); + + RecordEqualiser equaliser = + generated.newInstance(Thread.currentThread().getContextClassLoader()); + + GenericRow nullRow1 = new GenericRow(1); + nullRow1.setField(0, null); + GenericRow nullRow2 = new GenericRow(1); + nullRow2.setField(0, null); + GenericRow nonNullRow = GenericRow.of(1); + + // Both null - should be equal + assertThat(equaliser.equals(nullRow1, nullRow2)).isTrue(); + // One null, one not - should not be equal + assertThat(equaliser.equals(nullRow1, nonNullRow)).isFalse(); + assertThat(equaliser.equals(nonNullRow, nullRow1)).isFalse(); + } +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testArrayField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testArrayField.java.expected new file mode 100644 index 0000000000..3a5b15122d --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testArrayField.java.expected @@ -0,0 +1,53 @@ +public final class ArrayFieldEqualiser$0 implements org.apache.fluss.codegen.types.RecordEqualiser { + private boolean arrEq$1(org.apache.fluss.row.InternalArray left, org.apache.fluss.row.InternalArray right) { + if (left instanceof org.apache.fluss.row.BinaryArray && right instanceof org.apache.fluss.row.BinaryArray) { + return left.equals(right); + } + if (left.size() != right.size()) { + return false; + } + for (int i = 0; i < left.size(); i++) { + if (left.isNullAt(i) && right.isNullAt(i)) { + continue; + } + if (left.isNullAt(i) || right.isNullAt(i)) { + return false; + } + int l = left.getInt(i); + int r = right.getInt(i); + if (l != r) { + return false; + } + } + return true; + } + + + public ArrayFieldEqualiser$0(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.InternalArray leftVal = left.getArray(0); + org.apache.fluss.row.InternalArray rightVal = right.getArray(0); + return arrEq$1(leftVal, rightVal); + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBinaryField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBinaryField.java.expected new file mode 100644 index 0000000000..344a74dfbd --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBinaryField.java.expected @@ -0,0 +1,29 @@ +public final class BinaryFieldEqualiser$19 implements org.apache.fluss.codegen.types.RecordEqualiser { + public BinaryFieldEqualiser$19(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + byte[] leftVal = left.getBinary(0, 5); + byte[] rightVal = right.getBinary(0, 5); + return java.util.Arrays.equals(leftVal, rightVal); + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBooleanField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBooleanField.java.expected new file mode 100644 index 0000000000..1f6109bc79 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBooleanField.java.expected @@ -0,0 +1,29 @@ +public final class BooleanFieldEqualiser$3 implements org.apache.fluss.codegen.types.RecordEqualiser { + public BooleanFieldEqualiser$3(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + boolean leftVal = left.getBoolean(0); + boolean rightVal = right.getBoolean(0); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBytesField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBytesField.java.expected new file mode 100644 index 0000000000..306fc08688 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testBytesField.java.expected @@ -0,0 +1,29 @@ +public final class BytesFieldEqualiser$8 implements org.apache.fluss.codegen.types.RecordEqualiser { + public BytesFieldEqualiser$8(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + byte[] leftVal = left.getBytes(0); + byte[] rightVal = right.getBytes(0); + return java.util.Arrays.equals(leftVal, rightVal); + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testDecimalField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testDecimalField.java.expected new file mode 100644 index 0000000000..30320666af --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testDecimalField.java.expected @@ -0,0 +1,29 @@ +public final class DecimalFieldEqualiser$13 implements org.apache.fluss.codegen.types.RecordEqualiser { + public DecimalFieldEqualiser$13(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.Decimal leftVal = left.getDecimal(0, 10, 2); + org.apache.fluss.row.Decimal rightVal = right.getDecimal(0, 10, 2); + return leftVal.compareTo(rightVal) == 0; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testDoubleField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testDoubleField.java.expected new file mode 100644 index 0000000000..85a384dec7 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testDoubleField.java.expected @@ -0,0 +1,29 @@ +public final class DoubleFieldEqualiser$17 implements org.apache.fluss.codegen.types.RecordEqualiser { + public DoubleFieldEqualiser$17(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + double leftVal = left.getDouble(0); + double rightVal = right.getDouble(0); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testIntField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testIntField.java.expected new file mode 100644 index 0000000000..743c67bdde --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testIntField.java.expected @@ -0,0 +1,29 @@ +public final class IntFieldEqualiser$15 implements org.apache.fluss.codegen.types.RecordEqualiser { + public IntFieldEqualiser$15(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + int leftVal = left.getInt(0); + int rightVal = right.getInt(0); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testLongField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testLongField.java.expected new file mode 100644 index 0000000000..6ee7a3e6a7 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testLongField.java.expected @@ -0,0 +1,29 @@ +public final class LongFieldEqualiser$9 implements org.apache.fluss.codegen.types.RecordEqualiser { + public LongFieldEqualiser$9(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + long leftVal = left.getLong(0); + long rightVal = right.getLong(0); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testMapField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testMapField.java.expected new file mode 100644 index 0000000000..7b8932cf18 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testMapField.java.expected @@ -0,0 +1,67 @@ +public final class MapFieldEqualiser$10 implements org.apache.fluss.codegen.types.RecordEqualiser { + private boolean mapEq$11(org.apache.fluss.row.InternalMap left, org.apache.fluss.row.InternalMap right) { + if (left.size() != right.size()) { + return false; + } + org.apache.fluss.row.InternalArray lk = left.keyArray(); + org.apache.fluss.row.InternalArray lv = left.valueArray(); + org.apache.fluss.row.InternalArray rk = right.keyArray(); + org.apache.fluss.row.InternalArray rv = right.valueArray(); + for (int i = 0; i < left.size(); i++) { + org.apache.fluss.row.BinaryString lKey = lk.getString(i); + boolean found = false; + for (int j = 0; j < right.size(); j++) { + org.apache.fluss.row.BinaryString rKey = rk.getString(j); + if (lKey.equals(rKey)) { + if (lv.isNullAt(i) && rv.isNullAt(j)) { + found = true; + break; + } + if (lv.isNullAt(i) || rv.isNullAt(j)) { + return false; + } + int lVal = lv.getInt(i); + int rVal = rv.getInt(j); + if (lVal != rVal) { + return false; + } + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + + public MapFieldEqualiser$10(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.InternalMap leftVal = left.getMap(0); + org.apache.fluss.row.InternalMap rightVal = right.getMap(0); + return mapEq$11(leftVal, rightVal); + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testMultipleFields.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testMultipleFields.java.expected new file mode 100644 index 0000000000..65d5ba9b0e --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testMultipleFields.java.expected @@ -0,0 +1,93 @@ +public final class MultipleFieldsEqualiser$2 implements org.apache.fluss.codegen.types.RecordEqualiser { + public MultipleFieldsEqualiser$2(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + result = result && equalsField1(left, right); + result = result && equalsField2(left, right); + result = result && equalsField3(left, right); + result = result && equalsField4(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + int leftVal = left.getInt(0); + int rightVal = right.getInt(0); + return leftVal == rightVal; + } + + + private boolean equalsField1(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(1); + boolean rightNull = right.isNullAt(1); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.BinaryString leftVal = ((org.apache.fluss.row.BinaryString) left.getString(1)); + org.apache.fluss.row.BinaryString rightVal = ((org.apache.fluss.row.BinaryString) right.getString(1)); + return leftVal.equals(rightVal); + } + + + private boolean equalsField2(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(2); + boolean rightNull = right.isNullAt(2); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + boolean leftVal = left.getBoolean(2); + boolean rightVal = right.getBoolean(2); + return leftVal == rightVal; + } + + + private boolean equalsField3(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(3); + boolean rightNull = right.isNullAt(3); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + long leftVal = left.getLong(3); + long rightVal = right.getLong(3); + return leftVal == rightVal; + } + + + private boolean equalsField4(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(4); + boolean rightNull = right.isNullAt(4); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + double leftVal = left.getDouble(4); + double rightVal = right.getDouble(4); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testNestedRowField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testNestedRowField.java.expected new file mode 100644 index 0000000000..ef77f30b33 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testNestedRowField.java.expected @@ -0,0 +1,34 @@ +public final class NestedRowFieldEqualiser$4 implements org.apache.fluss.codegen.types.RecordEqualiser { + private transient org.apache.fluss.codegen.GeneratedClass nestedEqualiser$6; + private org.apache.fluss.codegen.types.RecordEqualiser rowEq$7; + + public NestedRowFieldEqualiser$4(java.lang.Object[] references) throws Exception { + nestedEqualiser$6 = ((org.apache.fluss.codegen.GeneratedClass) references[0]); + rowEq$7 = (org.apache.fluss.codegen.types.RecordEqualiser) nestedEqualiser$6.newInstance(this.getClass().getClassLoader()); + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.InternalRow leftVal = left.getRow(0, 2); + org.apache.fluss.row.InternalRow rightVal = right.getRow(0, 2); + return rowEq$7.equals(leftVal, rightVal); + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testNullHandling.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testNullHandling.java.expected new file mode 100644 index 0000000000..85ed4f5129 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testNullHandling.java.expected @@ -0,0 +1,29 @@ +public final class NullHandlingEqualiser$12 implements org.apache.fluss.codegen.types.RecordEqualiser { + public NullHandlingEqualiser$12(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + int leftVal = left.getInt(0); + int rightVal = right.getInt(0); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testProjection.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testProjection.java.expected new file mode 100644 index 0000000000..87b4638a04 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testProjection.java.expected @@ -0,0 +1,42 @@ +public final class ProjectionEqualiser$16 implements org.apache.fluss.codegen.types.RecordEqualiser { + public ProjectionEqualiser$16(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean result = true; + result = result && equalsField1(left, right); + result = result && equalsField2(left, right); + return result; + } + + private boolean equalsField1(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(1); + boolean rightNull = right.isNullAt(1); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.BinaryString leftVal = ((org.apache.fluss.row.BinaryString) left.getString(1)); + org.apache.fluss.row.BinaryString rightVal = ((org.apache.fluss.row.BinaryString) right.getString(1)); + return leftVal.equals(rightVal); + } + + + private boolean equalsField2(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(2); + boolean rightNull = right.isNullAt(2); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + long leftVal = left.getLong(2); + long rightVal = right.getLong(2); + return leftVal == rightVal; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testStringField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testStringField.java.expected new file mode 100644 index 0000000000..7de741af12 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testStringField.java.expected @@ -0,0 +1,29 @@ +public final class StringFieldEqualiser$20 implements org.apache.fluss.codegen.types.RecordEqualiser { + public StringFieldEqualiser$20(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.BinaryString leftVal = ((org.apache.fluss.row.BinaryString) left.getString(0)); + org.apache.fluss.row.BinaryString rightVal = ((org.apache.fluss.row.BinaryString) right.getString(0)); + return leftVal.equals(rightVal); + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testTimestampLtzField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testTimestampLtzField.java.expected new file mode 100644 index 0000000000..ce0aed8fcb --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testTimestampLtzField.java.expected @@ -0,0 +1,29 @@ +public final class TimestampLtzFieldEqualiser$18 implements org.apache.fluss.codegen.types.RecordEqualiser { + public TimestampLtzFieldEqualiser$18(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.TimestampLtz leftVal = left.getTimestampLtz(0, 3); + org.apache.fluss.row.TimestampLtz rightVal = right.getTimestampLtz(0, 3); + return leftVal.compareTo(rightVal) == 0; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testTimestampNtzField.java.expected b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testTimestampNtzField.java.expected new file mode 100644 index 0000000000..39f95473cb --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/equaliser-code-generator/testTimestampNtzField.java.expected @@ -0,0 +1,29 @@ +public final class TimestampNtzFieldEqualiser$14 implements org.apache.fluss.codegen.types.RecordEqualiser { + public TimestampNtzFieldEqualiser$14(java.lang.Object[] references) throws Exception { + } + + @Override + public boolean equals(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + if (left instanceof org.apache.fluss.row.BinaryRow && right instanceof org.apache.fluss.row.BinaryRow) { + return left.equals(right); + } + boolean result = true; + result = result && equalsField0(left, right); + return result; + } + + private boolean equalsField0(org.apache.fluss.row.InternalRow left, org.apache.fluss.row.InternalRow right) { + boolean leftNull = left.isNullAt(0); + boolean rightNull = right.isNullAt(0); + if (leftNull && rightNull) { + return true; + } + if (leftNull || rightNull) { + return false; + } + org.apache.fluss.row.TimestampNtz leftVal = left.getTimestampNtz(0, 6); + org.apache.fluss.row.TimestampNtz rightVal = right.getTimestampNtz(0, 6); + return leftVal.compareTo(rightVal) == 0; + } + +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/classStructure.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/classStructure.java.expected new file mode 100644 index 0000000000..5281b3f085 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/classStructure.java.expected @@ -0,0 +1,2 @@ +public final class MyClass implements Serializable { +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/completeClass.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/completeClass.java.expected new file mode 100644 index 0000000000..e4cbb7ca42 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/completeClass.java.expected @@ -0,0 +1,28 @@ +public final class RecordEqualiser implements Equaliser { + private int fieldCount; + private final Object[] references; + + public RecordEqualiser(Object[] references) throws Exception { + this.references = references; + this.fieldCount = 0; + } + + @Override + public boolean equals(Object left, Object right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + boolean result = true; + for (int i = 0; i < fieldCount; i++) { + result = result && compareField(i, left, right); + } + return result; + } + + private boolean compareField(int idx, Object l, Object r) { + return l.equals(r); + } +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/constructor.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/constructor.java.expected new file mode 100644 index 0000000000..3a25409f7a --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/constructor.java.expected @@ -0,0 +1,6 @@ +public class TestClass { + public TestClass(int value, String name) throws Exception { + this.value = value; + this.name = name; + } +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/emptyRawCode.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/emptyRawCode.java.expected new file mode 100644 index 0000000000..b4288a444c --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/emptyRawCode.java.expected @@ -0,0 +1,3 @@ +public class Test { + private int x; +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/fields.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/fields.java.expected new file mode 100644 index 0000000000..b5acc32f43 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/fields.java.expected @@ -0,0 +1,6 @@ +public class TestClass { + private int count; + private final String name; + private boolean active = true; + private static final int MAX = 100; +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/ifElseIfElse.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/ifElseIfElse.java.expected new file mode 100644 index 0000000000..09f9263873 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/ifElseIfElse.java.expected @@ -0,0 +1,11 @@ +public class TestClass { + public String classify(int x) { + if (x > 0) { + return "positive"; + } else if (x < 0) { + return "negative"; + } else { + return "zero"; + } + } +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/methodWithOverride.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/methodWithOverride.java.expected new file mode 100644 index 0000000000..08b5131b9c --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/methodWithOverride.java.expected @@ -0,0 +1,6 @@ +public class TestClass { + @Override + public String toString() { + return "TestClass"; + } +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/nestedControlFlow.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/nestedControlFlow.java.expected new file mode 100644 index 0000000000..9322763f7f --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/nestedControlFlow.java.expected @@ -0,0 +1,15 @@ +public class TestClass { + public void process(int[][] matrix) { + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + if (matrix[i][j] < 0) { + continue; + } else if (matrix[i][j] == 0) { + break; + } else { + System.out.println(matrix[i][j]); + } + } + } + } +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/rawCode.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/rawCode.java.expected new file mode 100644 index 0000000000..aebb648888 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/rawCode.java.expected @@ -0,0 +1,11 @@ +public class TestClass { + // Class-level comment + private static final int CONST = 42; + +// Unindented comment at class level + public void test() { + // Multi-line raw + int x = 1; + int y = 2; + } +} diff --git a/fluss-codegen/src/test/resources/expected/java-code-builder/typeSafeApi.java.expected b/fluss-codegen/src/test/resources/expected/java-code-builder/typeSafeApi.java.expected new file mode 100644 index 0000000000..4618e98c52 --- /dev/null +++ b/fluss-codegen/src/test/resources/expected/java-code-builder/typeSafeApi.java.expected @@ -0,0 +1,12 @@ +public final class Calculator implements java.io.Serializable { + private int result; + + public Calculator(java.lang.Object[] refs) throws Exception { + this.result = 0; + } + + public int add(int a, int b) { + int sum = a + b; + return sum; + } +} diff --git a/fluss-common/src/main/java/org/apache/fluss/types/DataTypeChecks.java b/fluss-common/src/main/java/org/apache/fluss/types/DataTypeChecks.java index 15ec476a31..35fb3028df 100644 --- a/fluss-common/src/main/java/org/apache/fluss/types/DataTypeChecks.java +++ b/fluss-common/src/main/java/org/apache/fluss/types/DataTypeChecks.java @@ -57,6 +57,50 @@ public static List getFieldTypes(DataType dataType) { return dataType.accept(FIELD_TYPES_EXTRACTOR); } + /** + * Checks whether the given {@link DataType} is a composite type, i.e., a Row, Array, or Map + * type. + * + * @param dataType the data type to check + * @return true if the type is a composite type + */ + public static boolean isCompositeType(DataType dataType) { + DataTypeRoot typeRoot = dataType.getTypeRoot(); + return typeRoot == DataTypeRoot.ROW + || typeRoot == DataTypeRoot.ARRAY + || typeRoot == DataTypeRoot.MAP; + } + + /** + * Returns the element type of an array type. + * + * @param dataType the array type + * @return the element type + */ + public static DataType getArrayElementType(DataType dataType) { + return ((ArrayType) dataType).getElementType(); + } + + /** + * Returns the key type of a map type. + * + * @param dataType the map type + * @return the key type + */ + public static DataType getMapKeyType(DataType dataType) { + return ((MapType) dataType).getKeyType(); + } + + /** + * Returns the value type of a map type. + * + * @param dataType the map type + * @return the value type + */ + public static DataType getMapValueType(DataType dataType) { + return ((MapType) dataType).getValueType(); + } + /** Checks whether two data types are equal including field ids for row types. */ public static boolean equalsWithFieldId(DataType original, DataType that) { return that.accept(new DataTypeEqualsWithFieldId(original)); diff --git a/fluss-common/src/main/java/org/apache/fluss/utils/TypeUtils.java b/fluss-common/src/main/java/org/apache/fluss/utils/TypeUtils.java index 04283c9d97..60dc8ca863 100644 --- a/fluss-common/src/main/java/org/apache/fluss/utils/TypeUtils.java +++ b/fluss-common/src/main/java/org/apache/fluss/utils/TypeUtils.java @@ -20,6 +20,7 @@ import org.apache.fluss.row.BinaryString; import org.apache.fluss.row.Decimal; import org.apache.fluss.types.DataType; +import org.apache.fluss.types.DataTypeRoot; import org.apache.fluss.types.DecimalType; import org.apache.fluss.types.LocalZonedTimestampType; import org.apache.fluss.types.TimestampType; @@ -30,6 +31,59 @@ /** Type related helper functions. */ public class TypeUtils { + + /** + * Checks whether the given {@link DataType} is a primitive type that can be compared using + * {@code ==}. + */ + public static boolean isPrimitive(DataType type) { + return isPrimitive(type.getTypeRoot()); + } + + /** + * Checks whether the given {@link DataTypeRoot} is a primitive type that can be compared using + * {@code ==}. + */ + public static boolean isPrimitive(DataTypeRoot root) { + switch (root) { + case BOOLEAN: + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case FLOAT: + case DOUBLE: + case DATE: + case TIME_WITHOUT_TIME_ZONE: + return true; + default: + return false; + } + } + + /** + * Checks whether the given {@link DataTypeRoot} is a binary type that should be compared using + * {@code Arrays.equals}. + */ + public static boolean isBinary(DataTypeRoot root) { + return root == DataTypeRoot.BINARY || root == DataTypeRoot.BYTES; + } + + /** + * Checks whether the given {@link DataTypeRoot} is a comparable type that should be compared + * using {@code compareTo}. + */ + public static boolean isComparable(DataTypeRoot root) { + switch (root) { + case DECIMAL: + case TIMESTAMP_WITHOUT_TIME_ZONE: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return true; + default: + return false; + } + } + public static Object castFromString(String s, DataType type) { BinaryString str = BinaryString.fromString(s); switch (type.getTypeRoot()) { diff --git a/fluss-test-coverage/pom.xml b/fluss-test-coverage/pom.xml index 16e942860e..b089a90dca 100644 --- a/fluss-test-coverage/pom.xml +++ b/fluss-test-coverage/pom.xml @@ -468,6 +468,8 @@ org.apache.flink.table.catalog.* + + org.apache.fluss.codegen.CodeGenException diff --git a/pom.xml b/pom.xml index 99558b790a..3d57175339 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ fluss-common + fluss-codegen fluss-metrics fluss-client fluss-rpc @@ -661,6 +662,8 @@ tools/releasing/release/** **/fluss-bin/conf/servers + + **/*.expected website/**/_category_.json website/package.json