diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index 2ca2528d5c0f..efde33f3465b 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -349,6 +349,8 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.UN_BASE64; import static org.apache.calcite.sql.fun.SqlLibraryOperators.URL_DECODE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.URL_ENCODE; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.UUIDV4; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.UUIDV7; import static org.apache.calcite.sql.fun.SqlLibraryOperators.XML_TRANSFORM; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ABS; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ACOS; @@ -871,6 +873,8 @@ void populate1() { defineReflective(RAND_INTEGER, BuiltInMethod.RAND_INTEGER.method, BuiltInMethod.RAND_INTEGER_SEED.method); defineReflective(RANDOM, BuiltInMethod.RAND.method); + defineReflective(UUIDV4, BuiltInMethod.UUIDV4.method); + defineReflective(UUIDV7, BuiltInMethod.UUIDV7.method); defineMethod(ACOS, BuiltInMethod.ACOS.method, NullPolicy.STRICT); defineMethod(ACOSD, BuiltInMethod.ACOSD.method, NullPolicy.STRICT); diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 4b9b48041fad..4519a9d25e69 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -138,6 +138,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BinaryOperator; import java.util.function.Consumer; @@ -329,6 +330,38 @@ public static boolean throwUnless(boolean condition, String message) { return condition; } + /** Generates a random UUID (version 4). Implements the SQL UUIDV4() function. */ + @NonDeterministic + public static UUID uuidv4() { + return UUID.randomUUID(); + } + + /** + * Generates a time-ordered UUID (version 7). Implements the SQL UUIDV7() function. + * + *
This static helper is a single-call, stateless variant: it produces a + * valid RFC 9562 UUID v7 but does not guarantee monotonic ordering across + * multiple calls in the same millisecond. For proper per-query monotonicity + * the code-generator uses {@link UuidFunction#uuidv7()} instead. + * + *
128-bit layout: 48-bit unix_ts_ms | 4-bit ver=7 | 12-bit rand_a | + * 2-bit var=10 | 62-bit rand_b. + */ + @NonDeterministic + public static UUID uuidv7() { + final ThreadLocalRandom rng = ThreadLocalRandom.current(); + final long millis = System.currentTimeMillis(); + // rand_a: 12 random bits (bits [11:0] of MSB) + final long randA = rng.nextLong() & 0x0FFFL; + // rand_b: 62 random bits (bits [61:0] of LSB) + final long randB = rng.nextLong() & 0x3FFF_FFFF_FFFF_FFFFL; + // MSB: timestamp(48) | version(4)=7 | rand_a(12) + final long msb = (millis << 16) | 0x7000L | randA; + // LSB: variant(2)=0b10 | rand_b(62) + final long lsb = 0x8000_0000_0000_0000L | randB; + return new UUID(msb, lsb); + } + public static String uuidToString(UUID uuid) { return uuid.toString(); } diff --git a/core/src/main/java/org/apache/calcite/runtime/UuidFunction.java b/core/src/main/java/org/apache/calcite/runtime/UuidFunction.java new file mode 100644 index 000000000000..f0fb5bb8d865 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/UuidFunction.java @@ -0,0 +1,145 @@ +/* + * 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.calcite.runtime; + +import org.apache.calcite.linq4j.function.Deterministic; + +import java.security.SecureRandom; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Function object for {@code UUIDV4} and {@code UUIDV7}. + * + *
{@code UUIDV4} generates a random (version 4) UUID as per RFC 4122. + * {@code UUIDV7} generates a time-ordered (version 7) UUID as per RFC 9562, + * using the Monotonic Random method (Section 6.2, Method 2): + *
Monotonicity and thread-safety are achieved without a lock: the timestamp + * and rand_a counter are packed into a single {@link AtomicLong} and updated + * via a compare-and-swap (CAS) retry loop. + * + *
State packing layout (64 bits): + *
+ * [63:12] last unix_ts_ms (52 bits, enough for ~142 million years) + * [11: 0] rand_a counter (12 bits, 0–4095) + *+ * + *
Marked {@link Deterministic} so that the code generator instantiates one + * instance per query, not once per row. + */ +@SuppressWarnings("unused") +public class UuidFunction { + + /** Source of random bits. + * + *
RFC 9562 section 6.9 recommends that implementations use a + * cryptographically secure pseudorandom number generator, so that generated + * values are unguessable. {@link SecureRandom} is thread-safe, and is the + * same source that {@link UUID#randomUUID()} uses for {@code UUIDV4}. */ + private static final SecureRandom RANDOM = new SecureRandom(); + + /** Bitmask for the 12-bit rand_a field (bits [11:0]). */ + private static final long RAND_A_MASK = 0x0FFFL; + + /** Bitmask for the 62-bit rand_b field (bits [61:0]). */ + private static final long RAND_B_MASK = 0x3FFF_FFFF_FFFF_FFFFL; + + /** Bit pattern that encodes UUID version 7 in bits [15:12] of the MSB. */ + private static final long VERSION_7 = 0x7000L; + + /** Bit pattern that encodes the IETF variant (0b10) in bits [63:62] of the LSB. */ + private static final long VARIANT_IETF = 0x8000_0000_0000_0000L; + + /** + * Packed monotonic state: high 52 bits = last timestamp (ms), low 12 bits = seqA. + * Initial value 0 ensures the first call's real timestamp always exceeds it, + * triggering a fresh random seed for rand_a. + */ + private final AtomicLong state = new AtomicLong(0L); + + /** Creates a UuidFunction. + * + *
Marked deterministic so that the code generator instantiates one once + * per query, not once per row. */ + @Deterministic public UuidFunction() { + } + + /** Implements the {@code UUIDV4()} SQL function. + * Returns a random (version 4) UUID as per RFC 4122. */ + public UUID uuidv4() { + return UUID.randomUUID(); + } + + /** + * Implements the {@code UUIDV7()} SQL function. + * Returns a time-ordered (version 7) UUID as per RFC 9562. + * + *
128-bit layout (MSB → LSB): + *
+ * |<--- unix_ts_ms (48) --->| ver(4)=7 |<rand_a(12)>| + * | var(2)=10 |<------------ rand_b (62) ------------>| + *+ * + *
Thread-safety is achieved lock-free via a CAS loop on {@link #state}. + * In the common case (no contention) the loop executes exactly once. + */ + public UUID uuidv7() { + long ms; + long seqA; + long current; + long next; + do { + current = state.get(); + final long lastMs = current >>> 12; // high 52 bits + final long lastSeq = current & RAND_A_MASK; // low 12 bits + ms = System.currentTimeMillis(); + if (ms > lastMs) { + // New millisecond: advance the clock and seed rand_a randomly so that + // the initial value of rand_a for this ms is unpredictable. + seqA = RANDOM.nextLong() & RAND_A_MASK; + } else { + // Same millisecond (or a clock regression): increment the counter to + // preserve monotonic order. + ms = lastMs; + seqA = lastSeq + 1; + if (seqA > RAND_A_MASK) { + // rand_a overflowed 12 bits; bump the logical clock by 1 ms. + ms = lastMs + 1; + seqA = 0; + } + } + next = (ms << 12) | seqA; + // CAS: if another thread has already updated state, retry with fresh reads. + } while (!state.compareAndSet(current, next)); + + // MSB: unix_ts_ms (48) | version 7 (4) | rand_a (12) + final long msb = (ms << 16) | VERSION_7 | seqA; + // LSB: IETF variant (2) | rand_b (62) — always fresh random bits + final long lsb = VARIANT_IETF | (RANDOM.nextLong() & RAND_B_MASK); + return new UUID(msb, lsb); + } +} diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java index 964fa03f8f91..5fc13c58e0dd 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/MssqlSqlDialect.java @@ -202,7 +202,13 @@ private static SqlNode createDatetimeCastSpec(String typeAlias, RelDataType type @Override public void unparseCall(SqlWriter writer, SqlCall call, int leftPrec, int rightPrec) { - if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) { + if (call.getOperator() == SqlLibraryOperators.UUIDV4) { + // UUIDV4() → NEWID() in SQL Server, which returns a random RFC 4122 + // version 4 UUID. + super.unparseCall(writer, + SqlLibraryOperators.NEWID.createCall(call.getParserPosition()), + leftPrec, rightPrec); + } else if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) { if (call.operandCount() != 3) { throw new IllegalArgumentException("MSSQL SUBSTRING requires FROM and FOR arguments"); } diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java index 9aab15f5e247..79bf2184c945 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java @@ -179,6 +179,16 @@ public OracleSqlDialect(Context context) { return; } + if (call.getOperator() == SqlLibraryOperators.UUIDV4) { + // UUIDV4() → UUID() in Oracle 23ai+. + // Oracle's UUID() returns RAW(16) and is RFC 4122 v4 compliant. + // UUIDV7 has no Oracle equivalent and is left untranslated. + super.unparseCall(writer, + SqlLibraryOperators.ORACLE_UUID.createCall(call.getParserPosition()), + leftPrec, rightPrec); + return; + } + if (call.getOperator().getSyntax() == SqlSyntax.FUNCTION_ID_CONSTANT) { writer.sep(call.getOperator().getName()); return; diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index 539d6b327fd0..f32c61f2c253 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -2824,4 +2824,56 @@ private static RelDataType deriveTypeMapFromEntries(SqlOperatorBinding opBinding OperandTypes.family(SqlTypeFamily.TIMESTAMP), OperandTypes.family(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.TIMESTAMP)), SqlFunctionCategory.TIMEDATE); + + /** The {@code UUIDV4()} function, which generates a random (version 4) UUID. + * This is a Calcite extension; not yet in any SQL standard. + * Dialect-specific targets: {@code NEWID()} on SQL Server, + * {@code uuidv4()} on PostgreSQL 17+, {@code UUID()} on Oracle 23ai+. */ + @LibraryOperator(libraries = {CALCITE}) + public static final SqlBasicFunction UUIDV4 = + SqlBasicFunction.create("UUIDV4", + ReturnTypes.explicit(SqlTypeName.UUID), + OperandTypes.NILADIC, + SqlFunctionCategory.SYSTEM) + .withDeterministic(false) + .withDynamic(true); + + /** The {@code UUIDV7()} function, which generates a time-ordered (version 7) UUID. + * This is a Calcite extension; not yet in any SQL standard. + * Dialect-specific targets: {@code uuidv7()} on PostgreSQL 17+. + * SQL Server and Oracle have no native equivalent. */ + @LibraryOperator(libraries = {CALCITE}) + public static final SqlBasicFunction UUIDV7 = + SqlBasicFunction.create("UUIDV7", + ReturnTypes.explicit(SqlTypeName.UUID), + OperandTypes.NILADIC, + SqlFunctionCategory.SYSTEM) + .withDeterministic(false) + .withDynamic(true); + + /** The {@code NEWID()} function for SQL Server. + * Generates a random RFC 4122 version 4 UUID, and may be used anywhere an + * expression is allowed. This is the SQL Server target when translating + * {@code UUIDV4()}. */ + @LibraryOperator(libraries = {MSSQL}) + public static final SqlBasicFunction NEWID = + SqlBasicFunction.create("NEWID", + ReturnTypes.explicit(SqlTypeName.UUID), + OperandTypes.NILADIC, + SqlFunctionCategory.SYSTEM) + .withDeterministic(false) + .withDynamic(true); + + /** The {@code UUID()} function for Oracle 23ai+. + * Generates an RFC 4122 version 4 (random) UUID. + * Returns {@code RAW(16)}; use {@code RAWTOHEX()} or cast to format as a string. + * This is the Oracle 23ai target when translating {@code UUIDV4()}. */ + @LibraryOperator(libraries = {ORACLE}) + public static final SqlBasicFunction ORACLE_UUID = + SqlBasicFunction.create("UUID", + ReturnTypes.explicit(SqlTypeName.UUID), + OperandTypes.NILADIC, + SqlFunctionCategory.SYSTEM) + .withDeterministic(false) + .withDynamic(true); } diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 629357b4e07e..c78d6aa9b0c4 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -105,6 +105,7 @@ import org.apache.calcite.runtime.SqlFunctions.FlatProductInputType; import org.apache.calcite.runtime.UrlFunctions; import org.apache.calcite.runtime.Utilities; +import org.apache.calcite.runtime.UuidFunction; import org.apache.calcite.runtime.XmlFunctions; import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; import org.apache.calcite.runtime.variant.VariantNull; @@ -618,6 +619,8 @@ public enum BuiltInMethod { RAND_INTEGER(RandomFunction.class, "randInteger", int.class), RAND_INTEGER_SEED(RandomFunction.class, "randIntegerSeed", int.class, int.class), + UUIDV4(UuidFunction.class, "uuidv4"), + UUIDV7(UuidFunction.class, "uuidv7"), SAFE_ADD(SqlFunctions.class, "safeAdd", double.class, double.class), SAFE_DIVIDE(SqlFunctions.class, "safeDivide", double.class, double.class), SAFE_MULTIPLY(SqlFunctions.class, "safeMultiply", double.class, double.class), diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 01f157893623..e84a85b4d4cd 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -12819,4 +12819,47 @@ public Sql schema(CalciteAssert.SchemaSpec schemaSpec) { + "FROM \"SCOTT\".\"EMP\"\n" + "WHERE \"DEPTNO\" = \"t1\".\"DEPTNO\")"); } + + /** Tests that {@code UUIDV4()} is translated to the correct dialect-specific + * function name when generating SQL for different databases. + * + *
{@code UUIDV4()} is a Calcite extension ({@link SqlLibrary#CALCITE}); + * {@code withLibrary(CALCITE)} is required so the parser recognises it. + * PostgreSQL 17+ passes through unchanged; SQL Server maps to + * {@code NEWID()}; Oracle 23ai+ maps to {@code UUID()}. */ + @Test void testUuidv4Func() { + final String query = "SELECT uuidv4() FROM \"employee\""; + final String expectedDefault = "SELECT UUIDV4()\n" + + "FROM \"foodmart\".\"employee\""; + final String expectedMssql = "SELECT NEWID()\n" + + "FROM [foodmart].[employee]"; + final String expectedOracle = "SELECT UUID()\n" + + "FROM \"foodmart\".\"employee\""; + // withLibrary(CALCITE) is required so that the parser recognises UUIDV4(). + sql(query).withLibrary(SqlLibrary.CALCITE) + .ok(expectedDefault) + .withMssql().ok(expectedMssql) + // PostgreSQL 17+ has native uuidv4(); name passes through unchanged. + .withPostgresql().ok(expectedDefault) + .withOracle().ok(expectedOracle); + } + + /** Tests that {@code UUIDV7()} is translated to the correct dialect-specific + * function name when generating SQL for different databases. + * + *
{@code UUIDV7()} is a Calcite extension ({@link SqlLibrary#CALCITE}); + * {@code withLibrary(CALCITE)} is required so the parser recognises it. + * PostgreSQL 17+ has native {@code uuidv7()}; MSSQL and Oracle have no + * native UUID v7 equivalent and emit {@code UUIDV7()} unchanged. */ + @Test void testUuidv7Func() { + final String query = "SELECT uuidv7() FROM \"employee\""; + final String expectedDefault = "SELECT UUIDV7()\n" + + "FROM \"foodmart\".\"employee\""; + // withLibrary(CALCITE) is required so that the parser recognises UUIDV7(). + // PostgreSQL 17+ has native uuidv7(). MSSQL and Oracle have no equivalent. + sql(query).withLibrary(SqlLibrary.CALCITE) + .ok(expectedDefault) + .withPostgresql().ok(expectedDefault) + .withMssql().ok("SELECT UUIDV7()\nFROM [foodmart].[employee]"); + } } diff --git a/core/src/test/java/org/apache/calcite/runtime/UuidFunctionTest.java b/core/src/test/java/org/apache/calcite/runtime/UuidFunctionTest.java new file mode 100644 index 000000000000..518b712b21e5 --- /dev/null +++ b/core/src/test/java/org/apache/calcite/runtime/UuidFunctionTest.java @@ -0,0 +1,112 @@ +/* + * 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.calcite.runtime; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +/** + * Tests for {@link UuidFunction}. + */ +class UuidFunctionTest { + + /** Verifies that {@code UUIDV4()} produces version-4, IETF-variant UUIDs. */ + @Test void testUuidv4Version() { + final UuidFunction f = new UuidFunction(); + for (int i = 0; i < 20; i++) { + final UUID uuid = f.uuidv4(); + assertThat("version", uuid.version(), is(4)); + assertThat("variant", uuid.variant(), is(2)); // IETF = 2 + } + } + + /** Verifies that {@code UUIDV7()} produces version-7, IETF-variant UUIDs. */ + @Test void testUuidv7Version() { + final UuidFunction f = new UuidFunction(); + for (int i = 0; i < 20; i++) { + final UUID uuid = f.uuidv7(); + assertThat("version", uuid.version(), is(7)); + assertThat("variant", uuid.variant(), is(2)); // IETF = 2 + } + } + + /** + * Verifies that the embedded timestamp in a {@code UUIDV7} UUID is within + * a reasonable window of the real clock (within ±1 second). + */ + @Test void testUuidv7Timestamp() { + final UuidFunction f = new UuidFunction(); + final long before = System.currentTimeMillis(); + final UUID uuid = f.uuidv7(); + final long after = System.currentTimeMillis(); + + // The 48-bit unix_ts_ms is the top 48 bits of mostSigBits, + // i.e. mostSigBits >>> 16. + final long embeddedMs = uuid.getMostSignificantBits() >>> 16; + + assertThat("embedded timestamp >= before", embeddedMs, + greaterThanOrEqualTo(before)); + // allow up to 1 s ahead (due to counter overflow advancing the clock) + assertThat("embedded timestamp <= after + 1000", embeddedMs, + lessThanOrEqualTo(after + 1000)); + } + + /** + * Verifies that UUIDs generated by consecutive calls to {@code UUIDV7()} + * are strictly increasing (monotonic) when compared as 128-bit unsigned + * integers, which is the sortability guarantee of RFC 9562 §6.2. + * + *
We generate 4 096 UUIDs in a tight loop to exercise the within-ms
+ * counter path.
+ */
+ @Test void testUuidv7Monotonicity() {
+ final UuidFunction f = new UuidFunction();
+ final int count = 4096;
+ final List