) 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:
+ *
+ *
+ * Define the interface in {@code org.apache.fluss.codegen.types}
+ * Create the generator class in this package
+ * Use {@link org.apache.fluss.codegen.CodeGeneratorContext} for managing reusable code
+ * Use {@link org.apache.fluss.codegen.JavaCodeBuilder} for building Java source code
+ * 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