Repository: eitsupi/neo-r-polars
Pull request #205: feat: `SQLContext` related functions
Author: login: etiennebacher
Created: 2025-03-18T15:40:03Z | Merged: 2025-03-22T08:55:07Z

This implements `$sql()` and the infrastructure for it. Not all [SQLContext functions](https://docs.pola.rs/api/python/dev/reference/sql/python_api.html#methods) are supported yet so that this PR is not too big.



Diff:
diff --git a/NAMESPACE b/NAMESPACE
index 0348f2392..b05c93c18 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -14,6 +14,7 @@ S3method("$",polars_lazy_group_by)
 S3method("$",polars_object)
 S3method("$",polars_rolling_group_by)
 S3method("$",polars_series)
+S3method("$",polars_sql_context)
 S3method("$<-",polars_object)
 S3method("$<-",savvy_neopolars__sealed)
 S3method("%%",polars_expr)
@@ -136,6 +137,7 @@ S3method(print,PlRDataType__bundle)
 S3method(print,PlRExpr__bundle)
 S3method(print,PlRLazyFrame__bundle)
 S3method(print,PlRLazyGroupBy__bundle)
+S3method(print,PlRSQLContext__bundle)
 S3method(print,PlRSeries__bundle)
 S3method(print,PlRThen__bundle)
 S3method(print,PlRWhen__bundle)
@@ -155,6 +157,7 @@ S3method(utils::.DollarNames,polars_lazy_frame)
 S3method(utils::.DollarNames,polars_lazy_group_by)
 S3method(utils::.DollarNames,polars_rolling_group_by)
 S3method(utils::.DollarNames,polars_series)
+S3method(utils::.DollarNames,polars_sql_context)
 S3method(wrap,PlRChainedThen)
 S3method(wrap,PlRChainedWhen)
 S3method(wrap,PlRDataFrame)
@@ -162,6 +165,7 @@ S3method(wrap,PlRDataType)
 S3method(wrap,PlRExpr)
 S3method(wrap,PlRLazyFrame)
 S3method(wrap,PlRLazyGroupBy)
+S3method(wrap,PlRSQLContext)
 S3method(wrap,PlRSeries)
 S3method(wrap,PlRThen)
 S3method(wrap,PlRWhen)
diff --git a/R/000-wrappers.R b/R/000-wrappers.R
index 8e2142186..25a1ac118 100644
--- a/R/000-wrappers.R
+++ b/R/000-wrappers.R
@@ -4021,6 +4021,56 @@ class(`PlRLazyGroupBy`) <- c("PlRLazyGroupBy__bundle", "savvy_neopolars__sealed"
   cat('PlRLazyGroupBy\n')
 }
 
+### wrapper functions for PlRSQLContext
+
+`PlRSQLContext_execute` <- function(self) {
+  function(`query`) {
+    .savvy_wrap_PlRLazyFrame(.Call(savvy_PlRSQLContext_execute__impl, `self`, `query`))
+  }
+}
+
+`PlRSQLContext_get_tables` <- function(self) {
+  function() {
+    .Call(savvy_PlRSQLContext_get_tables__impl, `self`)
+  }
+}
+
+`PlRSQLContext_register` <- function(self) {
+  function(`name`, `lf`) {
+    `lf` <- .savvy_extract_ptr(`lf`, "PlRLazyFrame")
+    invisible(.Call(savvy_PlRSQLContext_register__impl, `self`, `name`, `lf`))
+  }
+}
+
+`.savvy_wrap_PlRSQLContext` <- function(ptr) {
+  e <- new.env(parent = emptyenv())
+  e$.ptr <- ptr
+  e$`execute` <- `PlRSQLContext_execute`(ptr)
+  e$`get_tables` <- `PlRSQLContext_get_tables`(ptr)
+  e$`register` <- `PlRSQLContext_register`(ptr)
+
+  class(e) <- c("PlRSQLContext", "savvy_neopolars__sealed")
+  e
+}
+
+
+
+`PlRSQLContext` <- new.env(parent = emptyenv())
+
+### associated functions for PlRSQLContext
+
+`PlRSQLContext`$`new` <- function() {
+  .savvy_wrap_PlRSQLContext(.Call(savvy_PlRSQLContext_new__impl))
+}
+
+
+class(`PlRSQLContext`) <- c("PlRSQLContext__bundle", "savvy_neopolars__sealed")
+
+#' @export
+`print.PlRSQLContext__bundle` <- function(x, ...) {
+  cat('PlRSQLContext\n')
+}
+
 ### wrapper functions for PlRSeries
 
 `PlRSeries_add` <- function(self) {
diff --git a/R/generated-s3methods-dollar.R b/R/generated-s3methods-dollar.R
index 0d427770f..5a668f2c6 100644
--- a/R/generated-s3methods-dollar.R
+++ b/R/generated-s3methods-dollar.R
@@ -251,3 +251,31 @@
 
   filtered_names[!startsWith(filtered_names, "_")]
 }
+
+#' @export
+`$.polars_sql_context` <- function(x, name) {
+  member_names <- ls(x, all.names = TRUE)
+  method_names <- names(polars_sql_context__methods)
+
+  if (name %in% member_names) {
+    env_get(x, name)
+  } else if (name %in% method_names) {
+    fn <- polars_sql_context__methods[[name]]
+    self <- x
+    environment(fn) <- environment()
+    fn
+  } else {
+    NextMethod()
+  }
+}
+
+#' @exportS3Method utils::.DollarNames
+`.DollarNames.polars_sql_context` <- function(x, pattern = "") {
+  member_names <- ls(x, all.names = TRUE)
+  method_names <- names(polars_sql_context__methods)
+
+  all_names <- union(member_names, method_names)
+  filtered_names <- findMatches(pattern, all_names)
+
+  filtered_names[!startsWith(filtered_names, "_")]
+}
diff --git a/R/lazyframe-frame.R b/R/lazyframe-frame.R
index c69ef8ca3..acf329171 100644
--- a/R/lazyframe-frame.R
+++ b/R/lazyframe-frame.R
@@ -2560,4 +2560,49 @@ lazyframe__describe <- function(
   })
 }
 
+#' Execute a SQL query against the LazyFrame
+#'
+#' @inheritParams rlang::args_dots_empty
+#' @param query SQL query to execute.
+#' @param table_name Optionally provide an explicit name for the table that
+#' represents the calling frame (defaults to `"self"`).
+#'
+#' @details
+#' The calling frame is automatically registered as a table in the SQL context
+#' under the name `"self"`. If you want access to the DataFrames and LazyFrames
+#' found in the current globals, use the top-level [`pl$sql()`][pl__sql].
+#'
+#' More control over registration and execution behaviour is available by using
+#' the [`SQLContext`][pl__SQLContext] object.
+#'
+#' @inherit as_polars_lf return
+#' @examples
+#' lf1 <- pl$LazyFrame(a = 1:3, b = 6:8, c = c("z", "y", "x"))
+#'
+#' # Query the LazyFrame using SQL:
+#' lf1$sql("SELECT c, b FROM self WHERE a > 1")$collect()
+#'
+#' # Apply SQL transforms (aliasing "self" to "frame") then filter natively
+#' # (you can freely mix SQL and native operations):
+#' lf1$sql(
+#'   query = "
+#'        SELECT
+#'           a,
+#'           (a % 2 == 0) AS a_is_even,
+#'           (b::float4 / 2) AS 'b/2',
+#'           CONCAT_WS(':', c, c, c) AS c_c_c
+#'        FROM frame
+#'        ORDER BY a
+#'  ",
+#'   table_name = "frame",
+#' )$filter(!pl$col("c_c_c")$str$starts_with("x"))$collect()
+lazyframe__sql <- function(query, ..., table_name = "self") {
+  wrap({
+    check_dots_empty0(...)
+    ctx <- pl$SQLContext()
+    ctx$register(name = table_name, frame = self)
+    ctx$execute(query)
+  })
+}
+
 # TODO-REWRITE: implement $deserialize() for LazyFrame
diff --git a/R/sql-context.R b/R/sql-context.R
new file mode 100644
index 000000000..84f9701e4
--- /dev/null
+++ b/R/sql-context.R
@@ -0,0 +1,150 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(x) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+#' Initialize a new `SQLContext`
+#'
+#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Elements that are known in the
+#' current `SQLContext`. It accepts any R object that can be converted to a
+#' LazyFrame via `as_polars_lf()`. All elements must be named.
+#'
+# TODO: add this param (see commit that dropped incomplete implementation in
+# https://github.com/eitsupi/neo-r-polars/pull/205)
+# @param .register_globals Register compatible objects found in the global
+# environment, automatically mapping their name to a table name. To register
+# other objects, pass them explicitly in `...`, or call the `execute_global`
+# class function.
+#'
+#' @return An object of class `"polars_sql_context"`
+#' @examples
+#' pl$SQLContext(mtcars = mtcars)
+#'
+#' pl$SQLContext(mtcars = mtcars, a = data.frame(x = 1))
+pl__SQLContext <- function(...) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+#' Register a single frame as a table, using the given name
+#'
+#' @param name Name of the table.
+#' @param frame Object to associate with this table name.
+#'
+#' @inherit pl__SQLContext return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' ctx <- pl$SQLContext()
+#' ctx$register("frame_data", df)$execute("SELECT * FROM frame_data")$collect()
+sql_context__register <- function(name, frame = NULL) {
+  wrap({
+    frame <- as_polars_lf(frame)
+    self$`_ctxt`$register(name, frame$`_ldf`)
+    self
+  })
+}
+
+#' Register multiple eager/lazy frames as tables, using the associated names
+#'
+#' @inherit pl__SQLContext params return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' df2 <- pl$DataFrame(x = 2)
+#' df3 <- pl$DataFrame(x = 3)
+#'
+#' ctx <- pl$SQLContext()
+#' ctx$register_many(tab1 = df, tab2 = df2, tab3 = df3)
+sql_context__register_many <- function(...) {
+  wrap({
+    frames <- list2(...)
+    if (!is_named2(frames)) {
+      abort("All frames in `...` must be named.", call = caller_env(3))
+    }
+    for (i in seq_along(frames)) {
+      self$register(names(frames)[i], frames[[i]])
+    }
+    self
+  })
+}
+
+#' Return a list of the registered table names
+#'
+#' @details
+#' This method will return the same values as the "SHOW TABLES" SQL statement,
+#' but as a vector instead of a frame.
+#'
+#' @return A character vector
+#' @examples
+#' # Executing as SQL:
+#' frame_data <- pl$DataFrame(x = 1)
+#' ctx <- pl$SQLContext(hello_world=frame_data, foo = data.frame(x = 2))
+#' ctx$execute("SHOW TABLES")$collect()
+#'
+#' # Calling the method:
+#' ctx$tables()
+sql_context__tables <- function() {
+  wrap({
+    self$`_ctxt`$get_tables() |>
+      sort()
+  })
+}
+
+#' Parse the given SQL query and execute it against the registered frame data
+#'
+#' @param query A valid string SQL query.
+#'
+#' @inherit as_polars_lf return
+#' @examples
+#' # Declare frame data and register with a SQLContext:
+#' df <- pl$DataFrame(
+#'   title = c(
+#'     "The Godfather",
+#'     "The Dark Knight",
+#'     "Schindler's List",
+#'     "Pulp Fiction",
+#'     "The Shawshank Redemption"
+#'   ),
+#'   release_year = c(1972, 2008, 1993, 1994, 1994),
+#'   budget = c(6 * 1e6, 185 * 1e6, 22 * 1e6, 8 * 1e6, 25 * 1e6),
+#'   gross = c(134821952, 533316061, 96067179, 107930000, 28341469),
+#'   imdb_score = c(9.2, 9, 8.9, 8.9, 9.3)
+#' )
+#'
+#' ctx <- pl$SQLContext(films = df)
+#' ctx$execute(
+#'   "
+#'      SELECT title, release_year, imdb_score
+#'      FROM films
+#'      WHERE release_year > 1990
+#'      ORDER BY imdb_score DESC
+#'      "
+#' )$collect()
+#'
+#' # Execute a GROUP BY query:
+#' ctx$execute(
+#'   "
+#'   SELECT
+#'        MAX(release_year / 10) * 10 AS decade,
+#'        SUM(gross) AS total_gross,
+#'        COUNT(title) AS n_films,
+#'   FROM films
+#'   GROUP BY (release_year / 10) -- decade
+#'   ORDER BY total_gross DESC
+#'   "
+#' )$collect()
+sql_context__execute <- function(query) {
+  wrap({
+    self$`_ctxt`$execute(query)
+  })
+}
diff --git a/R/zzz.R b/R/zzz.R
index cc3208cd2..5c8095111 100644
--- a/R/zzz.R
+++ b/R/zzz.R
@@ -74,7 +74,8 @@ POLARS_STORE_ENVS <- list(
   "dataframe__" = polars_dataframe__methods,
   "groupby__" = polars_groupby__methods,
   "rolling_groupby__" = polars_rolling_groupby__methods,
-  "group_by_dynamic__" = polars_group_by_dynamic__methods
+  "group_by_dynamic__" = polars_group_by_dynamic__methods,
+  "sql_context__" = polars_sql_context__methods
 )
 
 lapply(names(POLARS_STORE_ENVS), function(name) {
diff --git a/dev/generate-r-files/data/class-names.tsv b/dev/generate-r-files/data/class-names.tsv
index 31a3e553a..2af114ec4 100644
--- a/dev/generate-r-files/data/class-names.tsv
+++ b/dev/generate-r-files/data/class-names.tsv
@@ -8,3 +8,4 @@ polars_expr	polars_expr__methods
 polars_lazy_frame	polars_lazyframe__methods
 polars_lazy_group_by	polars_lazygroupby__methods
 polars_series	polars_series__methods
+polars_sql_context	polars_sql_context__methods
diff --git a/man/lazyframe__sql.Rd b/man/lazyframe__sql.Rd
new file mode 100644
index 000000000..878528d68
--- /dev/null
+++ b/man/lazyframe__sql.Rd
@@ -0,0 +1,51 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/lazyframe-frame.R
+\name{lazyframe__sql}
+\alias{lazyframe__sql}
+\title{Execute a SQL query against the LazyFrame}
+\usage{
+lazyframe__sql(query, ..., table_name = "self")
+}
+\arguments{
+\item{query}{SQL query to execute.}
+
+\item{...}{These dots are for future extensions and must be empty.}
+
+\item{table_name}{Optionally provide an explicit name for the table that
+represents the calling frame (defaults to \code{"self"}).}
+}
+\value{
+A polars \link{LazyFrame}
+}
+\description{
+Execute a SQL query against the LazyFrame
+}
+\details{
+The calling frame is automatically registered as a table in the SQL context
+under the name \code{"self"}. If you want access to the DataFrames and LazyFrames
+found in the current globals, use the top-level \code{\link[=pl__sql]{pl$sql()}}.
+
+More control over registration and execution behaviour is available by using
+the \code{\link[=pl__SQLContext]{SQLContext}} object.
+}
+\examples{
+lf1 <- pl$LazyFrame(a = 1:3, b = 6:8, c = c("z", "y", "x"))
+
+# Query the LazyFrame using SQL:
+lf1$sql("SELECT c, b FROM self WHERE a > 1")$collect()
+
+# Apply SQL transforms (aliasing "self" to "frame") then filter natively
+# (you can freely mix SQL and native operations):
+lf1$sql(
+  query = "
+       SELECT
+          a,
+          (a \% 2 == 0) AS a_is_even,
+          (b::float4 / 2) AS 'b/2',
+          CONCAT_WS(':', c, c, c) AS c_c_c
+       FROM frame
+       ORDER BY a
+ ",
+  table_name = "frame",
+)$filter(!pl$col("c_c_c")$str$starts_with("x"))$collect()
+}
diff --git a/man/pl.Rd b/man/pl.Rd
index 68845abaf..be4470f7a 100644
--- a/man/pl.Rd
+++ b/man/pl.Rd
@@ -5,7 +5,7 @@
 \alias{pl}
 \title{Polars top-level function namespace}
 \format{
-An object of class \code{polars_object} of length 79.
+An object of class \code{polars_object} of length 80.
 }
 \usage{
 pl
diff --git a/man/pl__SQLContext.Rd b/man/pl__SQLContext.Rd
new file mode 100644
index 000000000..b2435e9f9
--- /dev/null
+++ b/man/pl__SQLContext.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/sql-context.R
+\name{pl__SQLContext}
+\alias{pl__SQLContext}
+\title{Initialize a new \code{SQLContext}}
+\usage{
+pl__SQLContext(...)
+}
+\arguments{
+\item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> Elements that are known in the
+current \code{SQLContext}. It accepts any R object that can be converted to a
+LazyFrame via \code{as_polars_lf()}. All elements must be named.}
+}
+\value{
+An object of class \code{"polars_sql_context"}
+}
+\description{
+Initialize a new \code{SQLContext}
+}
+\examples{
+pl$SQLContext(mtcars = mtcars)
+
+pl$SQLContext(mtcars = mtcars, a = data.frame(x = 1))
+}
diff --git a/man/sql_context__execute.Rd b/man/sql_context__execute.Rd
new file mode 100644
index 000000000..b4f3d57d4
--- /dev/null
+++ b/man/sql_context__execute.Rd
@@ -0,0 +1,56 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/sql-context.R
+\name{sql_context__execute}
+\alias{sql_context__execute}
+\title{Parse the given SQL query and execute it against the registered frame data}
+\usage{
+sql_context__execute(query)
+}
+\arguments{
+\item{query}{A valid string SQL query.}
+}
+\value{
+A polars \link{LazyFrame}
+}
+\description{
+Parse the given SQL query and execute it against the registered frame data
+}
+\examples{
+# Declare frame data and register with a SQLContext:
+df <- pl$DataFrame(
+  title = c(
+    "The Godfather",
+    "The Dark Knight",
+    "Schindler's List",
+    "Pulp Fiction",
+    "The Shawshank Redemption"
+  ),
+  release_year = c(1972, 2008, 1993, 1994, 1994),
+  budget = c(6 * 1e6, 185 * 1e6, 22 * 1e6, 8 * 1e6, 25 * 1e6),
+  gross = c(134821952, 533316061, 96067179, 107930000, 28341469),
+  imdb_score = c(9.2, 9, 8.9, 8.9, 9.3)
+)
+
+ctx <- pl$SQLContext(films = df)
+ctx$execute(
+  "
+     SELECT title, release_year, imdb_score
+     FROM films
+     WHERE release_year > 1990
+     ORDER BY imdb_score DESC
+     "
+)$collect()
+
+# Execute a GROUP BY query:
+ctx$execute(
+  "
+  SELECT
+       MAX(release_year / 10) * 10 AS decade,
+       SUM(gross) AS total_gross,
+       COUNT(title) AS n_films,
+  FROM films
+  GROUP BY (release_year / 10) -- decade
+  ORDER BY total_gross DESC
+  "
+)$collect()
+}
diff --git a/man/sql_context__register.Rd b/man/sql_context__register.Rd
new file mode 100644
index 000000000..913fdfd72
--- /dev/null
+++ b/man/sql_context__register.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/sql-context.R
+\name{sql_context__register}
+\alias{sql_context__register}
+\title{Register a single frame as a table, using the given name}
+\usage{
+sql_context__register(name, frame = NULL)
+}
+\arguments{
+\item{name}{Name of the table.}
+
+\item{frame}{Object to associate with this table name.}
+}
+\value{
+An object of class \code{"polars_sql_context"}
+}
+\description{
+Register a single frame as a table, using the given name
+}
+\examples{
+df <- pl$DataFrame(x = 1)
+ctx <- pl$SQLContext()
+ctx$register("frame_data", df)$execute("SELECT * FROM frame_data")$collect()
+}
diff --git a/man/sql_context__register_many.Rd b/man/sql_context__register_many.Rd
new file mode 100644
index 000000000..08416788a
--- /dev/null
+++ b/man/sql_context__register_many.Rd
@@ -0,0 +1,27 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/sql-context.R
+\name{sql_context__register_many}
+\alias{sql_context__register_many}
+\title{Register multiple eager/lazy frames as tables, using the associated names}
+\usage{
+sql_context__register_many(...)
+}
+\arguments{
+\item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> Elements that are known in the
+current \code{SQLContext}. It accepts any R object that can be converted to a
+LazyFrame via \code{as_polars_lf()}. All elements must be named.}
+}
+\value{
+An object of class \code{"polars_sql_context"}
+}
+\description{
+Register multiple eager/lazy frames as tables, using the associated names
+}
+\examples{
+df <- pl$DataFrame(x = 1)
+df2 <- pl$DataFrame(x = 2)
+df3 <- pl$DataFrame(x = 3)
+
+ctx <- pl$SQLContext()
+ctx$register_many(tab1 = df, tab2 = df2, tab3 = df3)
+}
diff --git a/man/sql_context__tables.Rd b/man/sql_context__tables.Rd
new file mode 100644
index 000000000..9c3160627
--- /dev/null
+++ b/man/sql_context__tables.Rd
@@ -0,0 +1,27 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/sql-context.R
+\name{sql_context__tables}
+\alias{sql_context__tables}
+\title{Return a list of the registered table names}
+\usage{
+sql_context__tables()
+}
+\value{
+A character vector
+}
+\description{
+Return a list of the registered table names
+}
+\details{
+This method will return the same values as the "SHOW TABLES" SQL statement,
+but as a vector instead of a frame.
+}
+\examples{
+# Executing as SQL:
+frame_data <- pl$DataFrame(x = 1)
+ctx <- pl$SQLContext(hello_world=frame_data, foo = data.frame(x = 2))
+ctx$execute("SHOW TABLES")$collect()
+
+# Calling the method:
+ctx$tables()
+}
diff --git a/src/init.c b/src/init.c
index 06cef049c..7ee8c4b89 100644
--- a/src/init.c
+++ b/src/init.c
@@ -2719,6 +2719,26 @@ SEXP savvy_PlRLazyGroupBy_tail__impl(SEXP self__, SEXP c_arg__n) {
     return handle_result(res);
 }
 
+SEXP savvy_PlRSQLContext_execute__impl(SEXP self__, SEXP c_arg__query) {
+    SEXP res = savvy_PlRSQLContext_execute__ffi(self__, c_arg__query);
+    return handle_result(res);
+}
+
+SEXP savvy_PlRSQLContext_get_tables__impl(SEXP self__) {
+    SEXP res = savvy_PlRSQLContext_get_tables__ffi(self__);
+    return handle_result(res);
+}
+
+SEXP savvy_PlRSQLContext_new__impl(void) {
+    SEXP res = savvy_PlRSQLContext_new__ffi();
+    return handle_result(res);
+}
+
+SEXP savvy_PlRSQLContext_register__impl(SEXP self__, SEXP c_arg__name, SEXP c_arg__lf) {
+    SEXP res = savvy_PlRSQLContext_register__ffi(self__, c_arg__name, c_arg__lf);
+    return handle_result(res);
+}
+
 SEXP savvy_PlRSeries_add__impl(SEXP self__, SEXP c_arg__other) {
     SEXP res = savvy_PlRSeries_add__ffi(self__, c_arg__other);
     return handle_result(res);
@@ -3468,6 +3488,10 @@ static const R_CallMethodDef CallEntries[] = {
     {"savvy_PlRLazyGroupBy_agg__impl", (DL_FUNC) &savvy_PlRLazyGroupBy_agg__impl, 2},
     {"savvy_PlRLazyGroupBy_head__impl", (DL_FUNC) &savvy_PlRLazyGroupBy_head__impl, 2},
     {"savvy_PlRLazyGroupBy_tail__impl", (DL_FUNC) &savvy_PlRLazyGroupBy_tail__impl, 2},
+    {"savvy_PlRSQLContext_execute__impl", (DL_FUNC) &savvy_PlRSQLContext_execute__impl, 2},
+    {"savvy_PlRSQLContext_get_tables__impl", (DL_FUNC) &savvy_PlRSQLContext_get_tables__impl, 1},
+    {"savvy_PlRSQLContext_new__impl", (DL_FUNC) &savvy_PlRSQLContext_new__impl, 0},
+    {"savvy_PlRSQLContext_register__impl", (DL_FUNC) &savvy_PlRSQLContext_register__impl, 3},
     {"savvy_PlRSeries_add__impl", (DL_FUNC) &savvy_PlRSeries_add__impl, 2},
     {"savvy_PlRSeries_as_str__impl", (DL_FUNC) &savvy_PlRSeries_as_str__impl, 1},
     {"savvy_PlRSeries_can_fast_explode_flag__impl", (DL_FUNC) &savvy_PlRSeries_can_fast_explode_flag__impl, 1},
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 72f4b1f4b..8c135d104 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -115,6 +115,7 @@ features = [
     "serde",
     "serde-lazy",
     "sign",
+    "sql",
     "streaming",
     "strings",
     "string_encoding",
diff --git a/src/rust/api.h b/src/rust/api.h
index 1032de472..52d784b30 100644
--- a/src/rust/api.h
+++ b/src/rust/api.h
@@ -550,6 +550,12 @@ SEXP savvy_PlRLazyGroupBy_agg__ffi(SEXP self__, SEXP c_arg__aggs);
 SEXP savvy_PlRLazyGroupBy_head__ffi(SEXP self__, SEXP c_arg__n);
 SEXP savvy_PlRLazyGroupBy_tail__ffi(SEXP self__, SEXP c_arg__n);
 
+// methods and associated functions for PlRSQLContext
+SEXP savvy_PlRSQLContext_execute__ffi(SEXP self__, SEXP c_arg__query);
+SEXP savvy_PlRSQLContext_get_tables__ffi(SEXP self__);
+SEXP savvy_PlRSQLContext_new__ffi(void);
+SEXP savvy_PlRSQLContext_register__ffi(SEXP self__, SEXP c_arg__name, SEXP c_arg__lf);
+
 // methods and associated functions for PlRSeries
 SEXP savvy_PlRSeries_add__ffi(SEXP self__, SEXP c_arg__other);
 SEXP savvy_PlRSeries_as_str__ffi(SEXP self__);
diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs
index 57766103f..256f2a805 100644
--- a/src/rust/src/lib.rs
+++ b/src/rust/src/lib.rs
@@ -13,6 +13,7 @@ mod prelude;
 mod r_threads;
 mod r_udf;
 mod series;
+mod sql;
 
 use crate::conversion::Wrap;
 use crate::dataframe::PlRDataFrame;
diff --git a/src/rust/src/sql.rs b/src/rust/src/sql.rs
new file mode 100644
index 000000000..103ae83f2
--- /dev/null
+++ b/src/rust/src/sql.rs
@@ -0,0 +1,55 @@
+use crate::{PlRLazyFrame, RPolarsErr};
+use polars::sql::SQLContext;
+use savvy::{savvy, EnvironmentSexp, Result, Sexp};
+
+#[savvy]
+#[repr(transparent)]
+#[derive(Clone)]
+pub struct PlRSQLContext {
+    pub context: SQLContext,
+}
+
+impl From<SQLContext> for PlRSQLContext {
+    fn from(context: SQLContext) -> Self {
+        PlRSQLContext { context }
+    }
+}
+
+impl TryFrom<EnvironmentSexp> for &PlRSQLContext {
+    type Error = String;
+
+    fn try_from(env: EnvironmentSexp) -> std::result::Result<Self, String> {
+        let ptr = env
+            .get(".ptr")
+            .expect("Failed to get `.ptr` from the object")
+            .ok_or("The object is not a valid polars SQL context")?;
+        <&PlRSQLContext>::try_from(ptr).map_err(|e| e.to_string())
+    }
+}
+
+#[savvy]
+impl PlRSQLContext {
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> Result<PlRSQLContext> {
+        Ok(PlRSQLContext {
+            context: SQLContext::new(),
+        })
+    }
+
+    pub fn execute(&mut self, query: &str) -> Result<PlRLazyFrame> {
+        Ok(self
+            .context
+            .execute(query)
+            .map_err(RPolarsErr::from)?
+            .into())
+    }
+
+    pub fn get_tables(&self) -> Result<Sexp> {
+        self.context.get_tables().try_into()
+    }
+
+    pub fn register(&mut self, name: &str, lf: &PlRLazyFrame) -> Result<()> {
+        self.context.register(name, lf.ldf.clone());
+        Ok(())
+    }
+}
diff --git a/tests/testthat/test-lazyframe-frame.R b/tests/testthat/test-lazyframe-frame.R
index c13fe63ae..c2372fa42 100644
--- a/tests/testthat/test-lazyframe-frame.R
+++ b/tests/testthat/test-lazyframe-frame.R
@@ -2278,3 +2278,70 @@ test_that("describe() works", {
   # min/max different depending on categorical ordering
   expect_snapshot(df$select(pl$col("cat")$cast(pl$Categorical("lexical")))$describe())
 })
+
+test_that("sql() works", {
+  lf <- pl$LazyFrame(a = 1:3, b = 6:8, c = c("z", "y", "x"))
+
+  expect_equal(
+    lf$sql("SELECT c, b FROM self WHERE a > 1")$collect(),
+    pl$DataFrame(c = c("y", "x"), b = 7:8)
+  )
+
+  # Can chain SQL and other functions
+  expect_equal(
+    lf$sql(
+      query = "
+         SELECT
+            a,
+            (a % 2 == 0) AS a_is_even,
+            (b::float4 / 2) AS 'b/2',
+            CONCAT_WS(':', c, c, c) AS c_c_c
+         FROM frame
+         ORDER BY a
+   ",
+      table_name = "frame",
+    )$filter(!pl$col("c_c_c")$str$starts_with("x"))$collect(),
+    pl$DataFrame(
+      a = 1:2,
+      a_is_even = c(FALSE, TRUE),
+      `b/2` = c(3, 3.5),
+      c_c_c = c("z:z:z", "y:y:y")
+    )$cast(`b/2` = pl$Float32)
+  )
+
+  # arg "table_name" works
+  expect_equal(
+    lf$sql(
+      query = "
+         SELECT
+            a
+         FROM foobar
+   ",
+      table_name = "foobar",
+    )$collect(),
+    pl$DataFrame(a = 1:3)
+  )
+  expect_error(
+    lf$sql(
+      query = "
+         SELECT
+            a
+         FROM wrong_name
+   ",
+      table_name = "foobar"
+    ),
+    "relation 'wrong_name' was not found"
+  )
+
+  expect_error(
+    lf$sql(
+      query = "
+         SELECT
+            a
+         FROM self
+      ",
+      a = 1
+    ),
+    "must be empty"
+  )
+})
diff --git a/tests/testthat/test-sql-context.R b/tests/testthat/test-sql-context.R
new file mode 100644
index 000000000..e755df373
--- /dev/null
+++ b/tests/testthat/test-sql-context.R
@@ -0,0 +1,13 @@
+test_that("pl$SQLContext() works", {
+  expect_equal(
+    pl$SQLContext(mtcars = mtcars, foo = data.frame(x = 1))$tables(),
+    c("foo", "mtcars")
+  )
+  expect_error(
+    pl$SQLContext(mtcars, foo = data.frame(x = 1)),
+    "All frames in `...` must be named"
+  )
+  # TODO: add message pattern when this is fixed
+  # https://github.com/eitsupi/neo-r-polars/issues/206
+  expect_error(pl$SQLContext(a = complex(1)))
+})


Reviews:
user: login: eitsupi
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-20T15:21:36Z
user: login: eitsupi
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-20T16:06:34Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:03:46Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:05:31Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:05:46Z
user: login: eitsupi
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-21T16:08:55Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:12:54Z
user: login: eitsupi
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-22T03:32:35Z
user: login: eitsupi
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-22T03:34:01Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T06:46:55Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T06:47:02Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T07:55:58Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T07:56:53Z
user: login: eitsupi
body: Could you not add `.register_globals` for now?
This is quite complex and should be added later. (In my opinion we need to be able to scan arbitrary environments by passing the environment as an argument)
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-22T08:12:37Z
user: login: eitsupi
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-22T08:29:53Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T08:40:50Z
user: login: etiennebacher
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T08:40:55Z
user: login: eitsupi
body: Looks good, thanks!
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-22T08:49:33Z

Review comments:
diff_hunk: @@ -0,0 +1,102 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
path: R/sql-context.R
user: login: eitsupi
body: What are these?
created_at: 2025-03-20T15:20:28Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,102 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new()
+    frames <- list2(...)
+    for (i in seq_along(frames)) {
+      frame <- ensure_lazyframe(frames[[i]])
+      self$register(names(frames)[i], frame$`_ldf`)
+    }
+    self
+  })
+}
+
+ensure_lazyframe <- function(obj) {
+  if (is_polars_df(obj)) {
+    obj$lazy()
+  } else if (is_polars_lf(obj)) {
+    obj
+  } else if (is_convertible_to_polars_series(obj)) {
+    as_polars_series(obj)$to_frame()$lazy()
+  } else {
+    abort("Cannot convert object to LazyFrame")
+  }
+}
path: R/sql-context.R
user: login: eitsupi
body: Is there any reason why `as_polars_lf()` should not be used?
created_at: 2025-03-20T15:21:09Z
author_association: OWNER
diff_hunk: @@ -2560,4 +2560,49 @@ lazyframe__describe <- function(
   })
 }
 
+#' Execute a SQL query against the LazyFrame
+#'
+#' @inheritParams rlang::args_dots_empty
+#' @param query SQL query to execute.
+#' @param table_name Optionally provide an explicit name for the table that
+#' represents the calling frame (defaults to `"self"`).
+#'
+#' @details
+#' The calling frame is automatically registered as a table in the SQL context
+#' under the name `"self"`. If you want access to the DataFrames and LazyFrames
+#' found in the current globals, use the top-level [`pl$sql()`][pl__sql].
+#'
+#' More control over registration and execution behaviour is available by using
+#' the [`SQLContext`][pl__SQLContext] object.
+#'
+#' @inherit as_polars_lf return
+#' @examples
+#' lf1 <- pl$LazyFrame(a = 1:3, b = 6:8, c = c("z", "y", "x"))
+#'
+#' # Query the LazyFrame using SQL:
+#' lf1$sql("SELECT c, b FROM self WHERE a > 1")$collect()
+#'
+#' # Apply SQL transforms (aliasing "self" to "frame") then filter natively
+#' # (you can freely mix SQL and native operations):
+#' lf1$sql(
+#'   query="
+#'       SELECT
+#'          a,
+#'          (a % 2 == 0) AS a_is_even,
+#'          (b::float4 / 2) AS 'b/2',
+#'          CONCAT_WS(':', c, c, c) AS c_c_c
+#'       FROM frame
+#'       ORDER BY a
+#' ",
+#' table_name="frame",
+#' )$filter(!pl$col("c_c_c")$str$starts_with("x"))$collect()
path: R/lazyframe-frame.R
user: login: eitsupi
body: Seems not formatted.
created_at: 2025-03-20T16:03:46Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,102 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new()
+    frames <- list2(...)
+    for (i in seq_along(frames)) {
+      frame <- ensure_lazyframe(frames[[i]])
+      self$register(names(frames)[i], frame$`_ldf`)
+    }
path: R/sql-context.R
user: login: eitsupi
body: Needs `register_many()`
created_at: 2025-03-20T16:06:16Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,102 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new()
+    frames <- list2(...)
+    for (i in seq_along(frames)) {
+      frame <- ensure_lazyframe(frames[[i]])
+      self$register(names(frames)[i], frame$`_ldf`)
+    }
+    self
+  })
+}
+
+ensure_lazyframe <- function(obj) {
+  if (is_polars_df(obj)) {
+    obj$lazy()
+  } else if (is_polars_lf(obj)) {
+    obj
+  } else if (is_convertible_to_polars_series(obj)) {
+    as_polars_series(obj)$to_frame()$lazy()
+  } else {
+    abort("Cannot convert object to LazyFrame")
+  }
+}
path: R/sql-context.R
user: login: etiennebacher
body: No, just oversight from me. I have replaced this block with `as_polars_lf(obj)`.
created_at: 2025-03-21T16:03:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,102 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new()
+    frames <- list2(...)
+    for (i in seq_along(frames)) {
+      frame <- ensure_lazyframe(frames[[i]])
+      self$register(names(frames)[i], frame$`_ldf`)
+    }
path: R/sql-context.R
user: login: etiennebacher
body: Done.
created_at: 2025-03-21T16:05:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2560,4 +2560,49 @@ lazyframe__describe <- function(
   })
 }
 
+#' Execute a SQL query against the LazyFrame
+#'
+#' @inheritParams rlang::args_dots_empty
+#' @param query SQL query to execute.
+#' @param table_name Optionally provide an explicit name for the table that
+#' represents the calling frame (defaults to `"self"`).
+#'
+#' @details
+#' The calling frame is automatically registered as a table in the SQL context
+#' under the name `"self"`. If you want access to the DataFrames and LazyFrames
+#' found in the current globals, use the top-level [`pl$sql()`][pl__sql].
+#'
+#' More control over registration and execution behaviour is available by using
+#' the [`SQLContext`][pl__SQLContext] object.
+#'
+#' @inherit as_polars_lf return
+#' @examples
+#' lf1 <- pl$LazyFrame(a = 1:3, b = 6:8, c = c("z", "y", "x"))
+#'
+#' # Query the LazyFrame using SQL:
+#' lf1$sql("SELECT c, b FROM self WHERE a > 1")$collect()
+#'
+#' # Apply SQL transforms (aliasing "self" to "frame") then filter natively
+#' # (you can freely mix SQL and native operations):
+#' lf1$sql(
+#'   query="
+#'       SELECT
+#'          a,
+#'          (a % 2 == 0) AS a_is_even,
+#'          (b::float4 / 2) AS 'b/2',
+#'          CONCAT_WS(':', c, c, c) AS c_c_c
+#'       FROM frame
+#'       ORDER BY a
+#' ",
+#' table_name="frame",
+#' )$filter(!pl$col("c_c_c")$str$starts_with("x"))$collect()
path: R/lazyframe-frame.R
user: login: etiennebacher
body: The `=` were indeed not formatted, but `air` doesn't change anything in the query itself.
created_at: 2025-03-21T16:05:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+ensure_lazyframe <- function(obj) {
+  as_polars_lf(obj)
+}
path: R/sql-context.R
user: login: eitsupi
body: What is this function needed for?
created_at: 2025-03-21T16:07:28Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,101 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+ensure_lazyframe <- function(obj) {
+  as_polars_lf(obj)
+}
path: R/sql-context.R
user: login: etiennebacher
body: After the change due to earlier comment, it's basically an alias for as_polars_lf(), I thought you'd like to have it for symmetry with py-polars: https://github.com/pola-rs/polars/blob/d95e343e04d47bcbeafc815a2d9855aa5099f1bb/py-polars/polars/sql/context.py#L471

But we can drop it ofc.
created_at: 2025-03-21T16:12:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +38,9 @@ sql_context__register <- function(name, frame = NULL) {
 sql_context__register_many <- function(...) {
   wrap({
     frames <- list2(...)
+    if (is.null(names(frames)) || any(names(frames) == "")) {
path: R/sql-context.R
user: login: eitsupi
body: It seems that `is_named2()` can be used here.
created_at: 2025-03-22T03:32:30Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,101 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+ensure_lazyframe <- function(obj) {
+  as_polars_lf(obj)
+}
path: R/sql-context.R
user: login: eitsupi
body: I think this is only necessary because Python does not have a generic function for such a use, and it is not needed here.
created_at: 2025-03-22T03:34:01Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,101 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(
+  x,
+  frames = NULL,
+  register_globals = FALSE
+) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+  self$frames <- frames
+  self$register_globals <- register_globals
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+pl__SQLContext <- function(..., register_globals = FALSE) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+ensure_lazyframe <- function(obj) {
+  as_polars_lf(obj)
+}
path: R/sql-context.R
user: login: etiennebacher
body: Done
created_at: 2025-03-22T06:46:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +38,9 @@ sql_context__register <- function(name, frame = NULL) {
 sql_context__register_many <- function(...) {
   wrap({
     frames <- list2(...)
+    if (is.null(names(frames)) || any(names(frames) == "")) {
path: R/sql-context.R
user: login: etiennebacher
body: Done
created_at: 2025-03-22T06:47:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(x) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+get_frame_locals <- function(all_compatible) {
path: R/sql-context.R
user: login: etiennebacher
body: Currently this only looks in the global env and not in all parent frames as in py-polars: https://github.com/pola-rs/polars/blob/main/py-polars/polars/_utils/various.py#L499

(`_get_stack_locals()` is called from `_get_frame_locals()`)

I found it surprisingly difficult to collect all objects in all parent frames without running into recursion issues.
created_at: 2025-03-22T07:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,37 @@
+test_that("pl$SQLContext() works", {
+  expect_equal(
+    pl$SQLContext(mtcars = mtcars, foo = data.frame(x = 1))$tables(),
+    c("foo", "mtcars")
+  )
+  expect_error(
+    pl$SQLContext(mtcars, foo = data.frame(x = 1)),
+    "All frames in `...` must be named"
+  )
+  # TODO: add message pattern when this is fixed
+  # https://github.com/eitsupi/neo-r-polars/issues/206
+  expect_error(pl$SQLContext(a = complex(1)))
+})
+
+test_that("arg '.register_globals' works", {
+  foo <- pl$DataFrame(x = 1)
+  assign("foo", foo, envir = global_env())
+  expect_equal(
path: tests/testthat/test-sql-context.R
user: login: etiennebacher
body: Not super clean but we need `foo` to be in the global environment (cf previous comment). AFAIK `withr` doesn't allow this and `assign()` is better than `<<-`.

created_at: 2025-03-22T07:56:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,157 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(x) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+#' Initialize a new `SQLContext`
+#'
+#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Elements that are known in the
+#' current `SQLContext`. It accepts any R object that can be converted to a
+#' LazyFrame via `as_polars_lf()`. All elements must be named.
+#'
+# TODO: add this param (see commit that dropped incomplete implementation in
+# https://github.com/eitsupi/neo-r-polars/pull/205)
+# @param .register_globals Register compatible objects found in the global
+# environment, automatically mapping their name to a table name. To register
+# other objects, pass them explicitly in `...`, or call the `execute_global`
+# class function.
+#'
+#' @return An object of class `"polars_sql_context"`
+#' @examples
+#' pl$SQLContext(mtcars = mtcars)
+#'
+#' pl$SQLContext(mtcars = mtcars, a = data.frame(x = 1))
+pl__SQLContext <- function(...) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+#' Register a single frame as a table, using the given name
+#'
+#' @param name Name of the table.
+#' @param frame Object to associate with this table name.
+#'
+#' @inherit pl__SQLContext return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' ctx <- pl$SQLContext()
+#' ctx$register("frame_data", df)$execute("SELECT * FROM frame_data")$collect()
+sql_context__register <- function(name, frame = NULL) {
+  wrap({
+    frame <- if (!is.null(frame)) {
+      as_polars_lf(frame)
+    } else {
+      pl$LazyFrame()
+    }
path: R/sql-context.R
user: login: eitsupi
body: I think this case separation is unnecessary since `as_polars_lf(NULL)` works.
created_at: 2025-03-22T08:28:48Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,157 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(x) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+#' Initialize a new `SQLContext`
+#'
+#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Elements that are known in the
+#' current `SQLContext`. It accepts any R object that can be converted to a
+#' LazyFrame via `as_polars_lf()`. All elements must be named.
+#'
+# TODO: add this param (see commit that dropped incomplete implementation in
+# https://github.com/eitsupi/neo-r-polars/pull/205)
+# @param .register_globals Register compatible objects found in the global
+# environment, automatically mapping their name to a table name. To register
+# other objects, pass them explicitly in `...`, or call the `execute_global`
+# class function.
+#'
+#' @return An object of class `"polars_sql_context"`
+#' @examples
+#' pl$SQLContext(mtcars = mtcars)
+#'
+#' pl$SQLContext(mtcars = mtcars, a = data.frame(x = 1))
+pl__SQLContext <- function(...) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+#' Register a single frame as a table, using the given name
+#'
+#' @param name Name of the table.
+#' @param frame Object to associate with this table name.
+#'
+#' @inherit pl__SQLContext return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' ctx <- pl$SQLContext()
+#' ctx$register("frame_data", df)$execute("SELECT * FROM frame_data")$collect()
+sql_context__register <- function(name, frame = NULL) {
+  wrap({
+    frame <- if (!is.null(frame)) {
+      as_polars_lf(frame)
+    } else {
+      pl$LazyFrame()
+    }
+    self$`_ctxt`$register(name, frame$`_ldf`)
+    self
+  })
+}
+
+#' Register multiple eager/lazy frames as tables, using the associated names
+#'
+#' @inherit pl__SQLContext params return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' df2 <- pl$DataFrame(x = 2)
+#' df3 <- pl$DataFrame(x = 3)
+#'
+#' ctx <- pl$SQLContext()
+#' ctx$register_many(tab1 = df, tab2 = df2, tab3 = df3)
+sql_context__register_many <- function(...) {
+  wrap({
+    frames <- list2(...)
+    if (length(frames) == 0) {
+      return(self)
+    }
path: R/sql-context.R
user: login: eitsupi
body: Is this necessary?
created_at: 2025-03-22T08:29:31Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,157 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(x) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+#' Initialize a new `SQLContext`
+#'
+#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Elements that are known in the
+#' current `SQLContext`. It accepts any R object that can be converted to a
+#' LazyFrame via `as_polars_lf()`. All elements must be named.
+#'
+# TODO: add this param (see commit that dropped incomplete implementation in
+# https://github.com/eitsupi/neo-r-polars/pull/205)
+# @param .register_globals Register compatible objects found in the global
+# environment, automatically mapping their name to a table name. To register
+# other objects, pass them explicitly in `...`, or call the `execute_global`
+# class function.
+#'
+#' @return An object of class `"polars_sql_context"`
+#' @examples
+#' pl$SQLContext(mtcars = mtcars)
+#'
+#' pl$SQLContext(mtcars = mtcars, a = data.frame(x = 1))
+pl__SQLContext <- function(...) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+#' Register a single frame as a table, using the given name
+#'
+#' @param name Name of the table.
+#' @param frame Object to associate with this table name.
+#'
+#' @inherit pl__SQLContext return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' ctx <- pl$SQLContext()
+#' ctx$register("frame_data", df)$execute("SELECT * FROM frame_data")$collect()
+sql_context__register <- function(name, frame = NULL) {
+  wrap({
+    frame <- if (!is.null(frame)) {
+      as_polars_lf(frame)
+    } else {
+      pl$LazyFrame()
+    }
path: R/sql-context.R
user: login: etiennebacher
body: Ah nice, thanks.
created_at: 2025-03-22T08:40:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,157 @@
+# The env for storing rolling_group_by methods
+polars_sql_context__methods <- new.env(parent = emptyenv())
+
+#' @export
+wrap.PlRSQLContext <- function(x) {
+  self <- new.env(parent = emptyenv())
+  self$`_ctxt` <- x
+
+  class(self) <- c("polars_sql_context", "polars_object")
+  self
+}
+
+#' Initialize a new `SQLContext`
+#'
+#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Elements that are known in the
+#' current `SQLContext`. It accepts any R object that can be converted to a
+#' LazyFrame via `as_polars_lf()`. All elements must be named.
+#'
+# TODO: add this param (see commit that dropped incomplete implementation in
+# https://github.com/eitsupi/neo-r-polars/pull/205)
+# @param .register_globals Register compatible objects found in the global
+# environment, automatically mapping their name to a table name. To register
+# other objects, pass them explicitly in `...`, or call the `execute_global`
+# class function.
+#'
+#' @return An object of class `"polars_sql_context"`
+#' @examples
+#' pl$SQLContext(mtcars = mtcars)
+#'
+#' pl$SQLContext(mtcars = mtcars, a = data.frame(x = 1))
+pl__SQLContext <- function(...) {
+  wrap({
+    self <- PlRSQLContext$new() |>
+      wrap()
+    self$register_many(...)
+    self
+  })
+}
+
+#' Register a single frame as a table, using the given name
+#'
+#' @param name Name of the table.
+#' @param frame Object to associate with this table name.
+#'
+#' @inherit pl__SQLContext return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' ctx <- pl$SQLContext()
+#' ctx$register("frame_data", df)$execute("SELECT * FROM frame_data")$collect()
+sql_context__register <- function(name, frame = NULL) {
+  wrap({
+    frame <- if (!is.null(frame)) {
+      as_polars_lf(frame)
+    } else {
+      pl$LazyFrame()
+    }
+    self$`_ctxt`$register(name, frame$`_ldf`)
+    self
+  })
+}
+
+#' Register multiple eager/lazy frames as tables, using the associated names
+#'
+#' @inherit pl__SQLContext params return
+#' @examples
+#' df <- pl$DataFrame(x = 1)
+#' df2 <- pl$DataFrame(x = 2)
+#' df3 <- pl$DataFrame(x = 3)
+#'
+#' ctx <- pl$SQLContext()
+#' ctx$register_many(tab1 = df, tab2 = df2, tab3 = df3)
+sql_context__register_many <- function(...) {
+  wrap({
+    frames <- list2(...)
+    if (length(frames) == 0) {
+      return(self)
+    }
path: R/sql-context.R
user: login: etiennebacher
body: Not anymore, it was at some point. Fixed.
created_at: 2025-03-22T08:40:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: eitsupi
created_at: 2025-03-22T08:14:02Z
body: In other words, there is a risk of tremendous performance degradation due to the need to inspect every object in the environment.
author_association: OWNER
user: login: etiennebacher
created_at: 2025-03-22T08:18:46Z
body: > Could you not add `.register_globals` for now?

Sure, happy to remove some complexity.
author_association: CONTRIBUTOR