Repository: ibis-project/ibis

Issue:
feat: expose a function for generating a new uuid
Some backends have functions for returning a newly generated UUID (e.g. postgres's [`gen_random_uuid`](https://www.postgresql.org/docs/current/functions-uuid.html) or duckdb's [`uuid`](https://duckdb.org/docs/sql/functions/utility.html)). Since Ibis includes support for `uuid` types, it might be nice to expose this as `ibis.uuid`.

*noted in a comment in https://github.com/ibis-project/ibis/issues/8256.*

Discussion:
jcrist: @jitingxu1 & @ncclementi - I pushed up what we did in the pairing session (plus a few ones after). I suggest we try to get this in and have y'all push up a follow-up adding the remaining backends.

Patch:
diff --git a/ibis/backends/bigquery/compiler.py b/ibis/backends/bigquery/compiler.py
index da2c0de87..da0be2d3d 100644
--- a/ibis/backends/bigquery/compiler.py
+++ b/ibis/backends/bigquery/compiler.py
@@ -122,6 +122,7 @@ class BigQueryCompiler(SQLGlotCompiler):
         ops.Levenshtein: "edit_distance",
         ops.Modulus: "mod",
         ops.RandomScalar: "rand",
+        ops.RandomUUID: "generate_uuid",
         ops.RegexReplace: "regexp_replace",
         ops.RegexSearch: "regexp_contains",
         ops.Time: "time",
diff --git a/ibis/backends/clickhouse/compiler.py b/ibis/backends/clickhouse/compiler.py
index 3ca46e925..3201e11ca 100644
--- a/ibis/backends/clickhouse/compiler.py
+++ b/ibis/backends/clickhouse/compiler.py
@@ -98,6 +98,7 @@ class ClickHouseCompiler(SQLGlotCompiler):
         ops.NullIf: "nullIf",
         ops.RStrip: "trimRight",
         ops.RandomScalar: "randCanonical",
+        ops.RandomUUID: "generateUUIDv4",
         ops.RegexReplace: "replaceRegexpAll",
         ops.RowNumber: "row_number",
         ops.StartsWith: "startsWith",
diff --git a/ibis/backends/duckdb/compiler.py b/ibis/backends/duckdb/compiler.py
index 208993fe8..06ceeacf7 100644
--- a/ibis/backends/duckdb/compiler.py
+++ b/ibis/backends/duckdb/compiler.py
@@ -51,6 +51,7 @@ class DuckDBCompiler(SQLGlotCompiler):
         ops.MapMerge: "map_concat",
         ops.MapValues: "map_values",
         ops.Mode: "mode",
+        ops.RandomUUID: "uuid",
         ops.TimeFromHMS: "make_time",
         ops.TypeOf: "typeof",
         ops.GeoPoint: "st_point",
diff --git a/ibis/backends/impala/compiler.py b/ibis/backends/impala/compiler.py
index e5831a152..08988386d 100644
--- a/ibis/backends/impala/compiler.py
+++ b/ibis/backends/impala/compiler.py
@@ -81,6 +81,7 @@ class ImpalaCompiler(SQLGlotCompiler):
         ops.Ln: "ln",
         ops.Log10: "log10",
         ops.Log2: "log2",
+        ops.RandomUUID: "uuid",
         ops.RStrip: "rtrim",
         ops.Strip: "trim",
         ops.TypeOf: "typeof",
diff --git a/ibis/backends/postgres/compiler.py b/ibis/backends/postgres/compiler.py
index 128496282..a90e56501 100644
--- a/ibis/backends/postgres/compiler.py
+++ b/ibis/backends/postgres/compiler.py
@@ -94,6 +94,7 @@ class PostgresCompiler(SQLGlotCompiler):
         ops.MapContains: "exist",
         ops.MapKeys: "akeys",
         ops.MapValues: "avals",
+        ops.RandomUUID: "gen_random_uuid",
         ops.RegexSearch: "regexp_like",
         ops.TimeFromHMS: "make_time",
     }
diff --git a/ibis/backends/risingwave/compiler.py b/ibis/backends/risingwave/compiler.py
index df34cda25..d6d20abe7 100644
--- a/ibis/backends/risingwave/compiler.py
+++ b/ibis/backends/risingwave/compiler.py
@@ -24,6 +24,7 @@ class RisingwaveCompiler(PostgresCompiler):
         (
             ops.DateFromYMD,
             ops.Mode,
+            ops.RandomUUID,
             *(
                 op
                 for op in ALL_OPERATIONS
diff --git a/ibis/backends/snowflake/compiler.py b/ibis/backends/snowflake/compiler.py
index a1e969435..814a86d50 100644
--- a/ibis/backends/snowflake/compiler.py
+++ b/ibis/backends/snowflake/compiler.py
@@ -80,6 +80,7 @@ class SnowflakeCompiler(SQLGlotCompiler):
         ops.Hash: "hash",
         ops.Median: "median",
         ops.Mode: "mode",
+        ops.RandomUUID: "uuid_string",
         ops.StringToTimestamp: "to_timestamp_tz",
         ops.TimeFromHMS: "time_from_parts",
         ops.TimestampFromYMDHMS: "timestamp_from_parts",
diff --git a/ibis/backends/sqlite/compiler.py b/ibis/backends/sqlite/compiler.py
index bd92b175f..3b18581b6 100644
--- a/ibis/backends/sqlite/compiler.py
+++ b/ibis/backends/sqlite/compiler.py
@@ -106,6 +106,7 @@ class SQLiteCompiler(SQLGlotCompiler):
         ops.Mode: "_ibis_mode",
         ops.Time: "time",
         ops.Date: "date",
+        ops.RandomUUID: "uuid",
     }
 
     def _aggregate(self, funcname: str, *args, where):
diff --git a/ibis/backends/sqlite/udf.py b/ibis/backends/sqlite/udf.py
index d4d646dbb..15cfdc46b 100644
--- a/ibis/backends/sqlite/udf.py
+++ b/ibis/backends/sqlite/udf.py
@@ -8,6 +8,7 @@ import operator
 from collections import defaultdict
 from typing import Any, Callable, NamedTuple
 from urllib.parse import parse_qs, urlsplit
+from uuid import uuid4
 
 try:
     import regex as re
@@ -222,6 +223,11 @@ def pi():
     return math.pi
 
 
+@udf(skip_if_exists=True)
+def uuid():
+    return str(uuid4())
+
+
 # Additional UDFS
 
 
diff --git a/ibis/backends/trino/compiler.py b/ibis/backends/trino/compiler.py
index e86e578a0..473df45f4 100644
--- a/ibis/backends/trino/compiler.py
+++ b/ibis/backends/trino/compiler.py
@@ -89,6 +89,7 @@ class TrinoCompiler(SQLGlotCompiler):
         ops.ExtractPath: "url_extract_path",
         ops.ExtractFragment: "url_extract_fragment",
         ops.ArrayPosition: "array_position",
+        ops.RandomUUID: "uuid",
     }
 
     def _aggregate(self, funcname: str, *args, where):
diff --git a/ibis/expr/api.py b/ibis/expr/api.py
index 9410737db..4dd072734 100644
--- a/ibis/expr/api.py
+++ b/ibis/expr/api.py
@@ -167,6 +167,7 @@ __all__ = (
     "trailing_range_window",
     "trailing_window",
     "union",
+    "uuid",
     "watermark",
     "where",
     "window",
@@ -690,6 +691,25 @@ def random() -> ir.FloatingScalar:
     return ops.RandomScalar().to_expr()
 
 
+def uuid() -> ir.UUIDScalar:
+    """Return a random UUID version 4 value.
+
+    Similar to [('uuid.uuid4`) in the Python standard library.
+
+    Examples
+    --------
+    >>> from ibis.interactive import *
+    >>> ibis.uuid()  # doctest: +SKIP
+    UUID('e57e927b-aed2-483b-9140-dc32a26cad95')
+
+    Returns
+    -------
+    UUIDScalar
+        Random UUID value expression
+    """
+    return ops.RandomUUID().to_expr()
+
+
 @overload
 def timestamp(
     value_or_year: int | ir.IntegerValue | Deferred,
diff --git a/ibis/expr/operations/generic.py b/ibis/expr/operations/generic.py
index a9e8ee454..2fb4218cd 100644
--- a/ibis/expr/operations/generic.py
+++ b/ibis/expr/operations/generic.py
@@ -193,6 +193,11 @@ class RandomScalar(Constant):
     dtype = dt.float64
 
 
+@public
+class RandomUUID(Constant):
+    dtype = dt.uuid
+
+
 @public
 class E(Constant):
     dtype = dt.float64


Test patch:
diff --git a/ibis/backends/tests/test_uuid.py b/ibis/backends/tests/test_uuid.py
index 36bf78154..a33102f4f 100644
--- a/ibis/backends/tests/test_uuid.py
+++ b/ibis/backends/tests/test_uuid.py
@@ -38,3 +38,25 @@ def test_uuid_literal(con, backend):
 
     with contextlib.suppress(com.OperationNotDefinedError):
         assert con.execute(expr.typeof()) == UUID_BACKEND_TYPE[backend_name]
+
+
+@pytest.mark.notimpl(
+    [
+        "datafusion",
+        "druid",
+        "exasol",
+        "flink",
+        "mssql",
+        "mysql",
+        "oracle",
+        "polars",
+        "pyspark",
+        "risingwave",
+    ],
+    raises=com.OperationNotDefinedError,
+)
+@pytest.mark.notimpl(["pandas", "dask"], raises=ValueError)
+def test_uuid_function(con):
+    obj = con.execute(ibis.uuid())
+    assert isinstance(obj, uuid.UUID)
+    assert obj.version == 4
diff --git a/ibis/tests/expr/test_uuid.py b/ibis/tests/expr/test_uuid.py
new file mode 100644
index 000000000..12cc99075
--- /dev/null
+++ b/ibis/tests/expr/test_uuid.py
@@ -0,0 +1,9 @@
+from __future__ import annotations
+
+import ibis
+
+
+def test_uuid():
+    u = ibis.uuid()
+    assert u.type().is_uuid()
+    assert isinstance(u.op().shape, ibis.expr.datashape.Scalar)