Skip to content

Proper bindings to rust library in R package#281

Merged
thomasp85 merged 19 commits intoposit-dev:mainfrom
thomasp85:r-bindings
Apr 17, 2026
Merged

Proper bindings to rust library in R package#281
thomasp85 merged 19 commits intoposit-dev:mainfrom
thomasp85:r-bindings

Conversation

@thomasp85
Copy link
Copy Markdown
Collaborator

@thomasp85 thomasp85 commented Mar 30, 2026

Fix #162

@eitsupi
Copy link
Copy Markdown

eitsupi commented Mar 31, 2026

I recommend referring to the sedonadb package to build inside monorepo.
https://github.com/apache/sedona-db

@thomasp85
Copy link
Copy Markdown
Collaborator Author

I'm afraid I'm not entirely sure what the relevance to this PR is?

@eitsupi
Copy link
Copy Markdown

eitsupi commented Mar 31, 2026

Oh, sorry. I just wanted to point out that, as you can see from the current CI failure in this type of monorepo, Rust-based R packages cannot pass R CMD checks without some workarounds.

@thomasp85
Copy link
Copy Markdown
Collaborator Author

Ah, gotcha. Thanks for the suggestion. I'm not worrying about CI with this PR just yet🙂

@thomasp85 thomasp85 marked this pull request as ready for review April 17, 2026 07:56
@thomasp85 thomasp85 requested a review from georgestagg April 17, 2026 08:39
Copy link
Copy Markdown
Collaborator

@georgestagg georgestagg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than what looks like a minor merge reversal, LGTM!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/validate.rs Outdated
Comment on lines +179 to +181
// not if the columns are valid. We skip this check for wildcards
// (either layer or global).
let is_annotation = matches!(
layer.source,
Some(crate::plot::types::DataSource::Annotation)
);
let has_wildcard =
layer.mappings.wildcard || (!is_annotation && plot.global_mappings.wildcard);
if !has_wildcard {
// Merge global mappings into a temporary copy for validation
// (mirrors execution-time merge, layer takes precedence)
let mut merged = layer.clone();
if !is_annotation {
for (aesthetic, value) in &plot.global_mappings.aesthetics {
merged
.mappings
.aesthetics
.entry(aesthetic.clone())
.or_insert(value.clone());
}
}
if let Err(e) = merged.validate_mapping(&plot.aesthetic_context, false) {
// not if the columns are valid. We skip this check for wildcards.
if !layer.mappings.wildcard {
if let Err(e) = layer.validate_mapping(&plot.aesthetic_context, false) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentional? It looks like a merge from main got reversed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, that is some merge conflict gone wrong because I also had a fix for the thing you fixed in main

Good catch

Comment thread src/validate.rs
Comment on lines -179 to -358
// not if the columns are valid. We skip this check for wildcards
// (either layer or global).
let is_annotation = matches!(
layer.source,
Some(crate::plot::types::DataSource::Annotation)
);
let has_wildcard =
layer.mappings.wildcard || (!is_annotation && plot.global_mappings.wildcard);
if !has_wildcard {
// Merge global mappings into a temporary copy for validation
// (mirrors execution-time merge, layer takes precedence)
let mut merged = layer.clone();
if !is_annotation {
for (aesthetic, value) in &plot.global_mappings.aesthetics {
merged
.mappings
.aesthetics
.entry(aesthetic.clone())
.or_insert(value.clone());
}
}
if let Err(e) = merged.validate_mapping(&plot.aesthetic_context, false) {
// not if the columns are valid. We skip this check for wildcards.
if !layer.mappings.wildcard {
if let Err(e) = layer.validate_mapping(&plot.aesthetic_context, false) {
errors.push(ValidationError {
message: format!("{}: {}", context, e),
location: None,
});
}
}

// Validate SETTING parameters
if let Err(e) = layer.validate_settings() {
errors.push(ValidationError {
message: format!("{}: {}", context, e),
location: None,
});
}
}
}

Ok(Validated {
sql: sql_part,
visual: viz_part,
has_visual,
tree: Some(source_tree.tree),
valid: errors.is_empty(),
errors,
warnings,
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_validate_with_visual() {
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y").unwrap();
assert!(validated.has_visual());
assert_eq!(validated.sql(), "SELECT 1 as x, 2 as y");
assert!(validated.visual().starts_with("VISUALISE"));
assert!(validated.tree().is_some());
assert!(validated.valid());
}

#[test]
fn test_validate_without_visual() {
let validated = validate("SELECT 1 as x, 2 as y").unwrap();
assert!(!validated.has_visual());
assert_eq!(validated.sql(), "SELECT 1 as x, 2 as y");
assert!(validated.visual().is_empty());
assert!(validated.tree().is_none());
assert!(validated.valid());
}

#[test]
fn test_validate_valid_query() {
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y").unwrap();
assert!(
validated.valid(),
"Expected valid query: {:?}",
validated.errors()
);
assert!(validated.errors().is_empty());
}

#[test]
fn test_validate_missing_required_aesthetic() {
// Point requires x and y, but we only provide x
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x").unwrap();
assert!(!validated.valid());
assert!(!validated.errors().is_empty());
assert!(validated.errors()[0].message.contains("y"));
}

#[test]
fn test_validate_syntax_error() {
let validated = validate("SELECT 1 VISUALISE DRAW invalidgeom").unwrap();
assert!(!validated.valid());
assert!(!validated.errors().is_empty());
}

#[test]
fn test_validate_sql_and_visual_content() {
let query = "SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y DRAW line MAPPING x AS x, y AS y";
let validated = validate(query).unwrap();

assert!(validated.has_visual());
assert_eq!(validated.sql(), "SELECT 1 as x, 2 as y");
assert!(validated.visual().contains("DRAW point"));
assert!(validated.visual().contains("DRAW line"));
assert!(validated.valid());
}

#[test]
fn test_validate_sql_only() {
let query = "SELECT 1 as x, 2 as y";
let validated = validate(query).unwrap();

// SQL-only queries should be valid (just syntax check)
assert!(validated.valid());
assert!(validated.errors().is_empty());
}

#[test]
fn test_validate_color_aesthetic_on_line() {
// color should be valid on line geom (has stroke)
let validated = validate(
"SELECT 1 as x, 2 as y VISUALISE DRAW line MAPPING x AS x, y AS y, region AS color",
)
.unwrap();
assert!(
validated.valid(),
"color should be accepted on line geom: {:?}",
validated.errors()
);
}

#[test]
fn test_validate_color_aesthetic_on_point() {
// color should be valid on point geom (has stroke + fill)
let validated = validate(
"SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y, cat AS color",
)
.unwrap();
assert!(
validated.valid(),
"color should be accepted on point geom: {:?}",
validated.errors()
);
}

#[test]
fn test_validate_colour_spelling() {
// British spelling 'colour' should work (normalized by parser to 'color')
let validated = validate(
"SELECT 1 as x, 2 as y VISUALISE DRAW line MAPPING x AS x, y AS y, region AS colour",
)
.unwrap();
assert!(
validated.valid(),
"colour (British) should be accepted: {:?}",
validated.errors()
);
}

#[test]
fn test_validate_global_color_mapping() {
// Global color mapping should validate correctly
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE x AS x, y AS y, region AS color DRAW line")
.unwrap();
assert!(
validated.valid(),
"global color mapping should be accepted: {:?}",
validated.errors()
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, should this be left in place?

@georgestagg
Copy link
Copy Markdown
Collaborator

Ah, wait, we seem to have lost plots height in docs:

Screenshot 2026-04-17 at 10 12 28

@thomasp85 thomasp85 merged commit 74dec91 into posit-dev:main Apr 17, 2026
15 checks passed
Comment thread ggsql-R/R/convert.R
Comment on lines +5 to +16
df_to_ipc <- function(df) {
# Convert factors to character — nanoarrow doesn't support dictionary IPC encoding
factor_cols <- vapply(df, is.factor, logical(1))
if (any(factor_cols)) {
df[factor_cols] <- lapply(df[factor_cols], as.character)
}
stream <- nanoarrow::as_nanoarrow_array_stream(df)
con <- rawConnection(raw(0), "wb")
on.exit(close(con))
nanoarrow::write_nanoarrow(stream, con)
rawConnectionValue(con)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to implement zero-copying using Arrow C streams?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proper R package bindings

3 participants