use serde::Serialize;
pub mod ast;
pub mod error;
pub mod parser;
pub mod sql;
#[cfg(any(feature = "postgres", feature = "wasm"))]
pub mod schema_cache;
#[cfg(feature = "wasm")]
pub mod wasm;
pub use ast::{
Cardinality, Column, ConflictAction, Count, DeleteParams, Direction, Field, Filter,
FilterOperator, FilterValue, InsertParams, InsertValues, ItemHint, ItemType, JsonOp, Junction,
LogicCondition, LogicOperator, LogicTree, Missing, Nulls, OnConflict, Operation, OrderTerm,
ParsedParams, Plurality, PreferOptions, Quantifier, Relationship, Resolution, ResolvedTable,
ReturnRepresentation, RpcParams, SelectItem, Table, UpdateParams,
};
pub use error::{Error, ParseError, SqlError};
pub use parser::{
field, get_profile_header, identifier, json_path, json_path_segment, logic_key,
parse_delete_params, parse_filter, parse_insert_params, parse_json_body, parse_logic,
parse_order, parse_order_term, parse_prefer_header, parse_qualified_table, parse_rpc_params,
parse_select, parse_update_params, reserved_key, resolve_schema, type_cast,
validate_insert_body, validate_update_body,
};
pub use sql::{QueryBuilder, QueryResult};
#[cfg(feature = "postgres")]
pub use schema_cache::{ForeignKey, RelationType, SchemaCache};
pub fn parse_query_string(query_string: &str) -> Result<ParsedParams, Error> {
let pairs: Vec<(String, String)> = query_string
.split('&')
.filter_map(|pair| {
let parts: Vec<&str> = pair.splitn(2, '=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect();
parse_params_from_pairs(pairs)
}
pub fn parse_params(
params: &std::collections::HashMap<String, String>,
) -> Result<ParsedParams, Error> {
let select_str = params.get("select").map(|s| s.to_string());
let order_str = params.get("order").map(|s| s.to_string());
let filters = parse_filters_from_map(params)?;
let limit = params.get("limit").and_then(|s| s.parse::<u64>().ok());
let offset = params.get("offset").and_then(|s| s.parse::<u64>().ok());
let mut parsed = ParsedParams::new().with_filters(filters);
if let Some(select_str) = select_str {
parsed = parsed.with_select(parse_select(&select_str)?);
}
if let Some(order_str) = order_str {
parsed = parsed.with_order(parse_order(&order_str)?);
}
if let Some(lim) = limit {
parsed = parsed.with_limit(lim);
}
if let Some(off) = offset {
parsed = parsed.with_offset(off);
}
Ok(parsed)
}
pub fn parse_params_from_pairs(pairs: Vec<(String, String)>) -> Result<ParsedParams, Error> {
let mut single_value_map = std::collections::HashMap::new();
let mut filter_pairs = Vec::new();
for (key, value) in pairs {
if parser::filter::reserved_key(&key) {
single_value_map.insert(key, value);
} else {
filter_pairs.push((key, value));
}
}
let select_str = single_value_map.get("select").map(|s| s.to_string());
let order_str = single_value_map.get("order").map(|s| s.to_string());
let limit = single_value_map
.get("limit")
.and_then(|s| s.parse::<u64>().ok());
let offset = single_value_map
.get("offset")
.and_then(|s| s.parse::<u64>().ok());
let filters = parse_filters_from_pairs(&filter_pairs)?;
let mut parsed = ParsedParams::new().with_filters(filters);
if let Some(select_str) = select_str {
parsed = parsed.with_select(parse_select(&select_str)?);
}
if let Some(order_str) = order_str {
parsed = parsed.with_order(parse_order(&order_str)?);
}
if let Some(lim) = limit {
parsed = parsed.with_limit(lim);
}
if let Some(off) = offset {
parsed = parsed.with_offset(off);
}
Ok(parsed)
}
pub fn to_sql(table: &str, params: &ParsedParams) -> Result<QueryResult, Error> {
if table.is_empty() {
return Err(Error::Sql(SqlError::EmptyTableName));
}
let mut builder = QueryBuilder::new();
builder.build_select(table, params).map_err(Error::Sql)
}
pub fn query_string_to_sql(table: &str, query_string: &str) -> Result<QueryResult, Error> {
let params = parse_query_string(query_string)?;
to_sql(table, ¶ms)
}
pub fn build_filter_clause(filters: &[LogicCondition]) -> Result<FilterClauseResult, Error> {
let mut builder = QueryBuilder::new();
builder.build_where_clause(filters).map_err(Error::Sql)?;
Ok(FilterClauseResult {
clause: builder.sql.clone(),
params: builder.params.clone(),
})
}
pub fn parse(
method: &str,
table: &str,
query_string: &str,
body: Option<&str>,
headers: Option<&std::collections::HashMap<String, String>>,
) -> Result<Operation, Error> {
if let Some(function_name) = table.strip_prefix("rpc/") {
if function_name.is_empty() {
return Err(Error::Parse(ParseError::InvalidTableName(
"RPC function name cannot be empty".to_string(),
)));
}
let _resolved_table = resolve_schema(function_name, method, headers)?;
let prefer = headers
.and_then(|h| {
h.get("Prefer")
.or_else(|| h.get("prefer"))
.or_else(|| h.get("PREFER"))
})
.map(|p| parse_prefer_header(p))
.transpose()?;
let params = parse_rpc_params(function_name, query_string, body)?;
return Ok(Operation::Rpc(params, prefer));
}
let _resolved_table = resolve_schema(table, method, headers)?;
let prefer = headers
.and_then(|h| {
h.get("Prefer")
.or_else(|| h.get("prefer"))
.or_else(|| h.get("PREFER"))
})
.map(|p| parse_prefer_header(p))
.transpose()?;
match method.to_uppercase().as_str() {
"GET" => {
let params = parse_query_string(query_string)?;
Ok(Operation::Select(params, prefer))
}
"POST" => {
let body = body.ok_or_else(|| {
Error::Parse(ParseError::InvalidInsertBody(
"Body is required for INSERT".to_string(),
))
})?;
let params = parse_insert_params(query_string, body)?;
Ok(Operation::Insert(params, prefer))
}
"PUT" => {
let body = body.ok_or_else(|| {
Error::Parse(ParseError::InvalidInsertBody(
"Body is required for PUT/upsert".to_string(),
))
})?;
let mut params = parse_insert_params(query_string, body)?;
if params.on_conflict.is_none() {
let conflict_columns = extract_conflict_columns_from_query(query_string);
if !conflict_columns.is_empty() {
params = params.with_on_conflict(OnConflict::do_update(conflict_columns));
}
}
Ok(Operation::Insert(params, prefer))
}
"PATCH" => {
let body = body.ok_or_else(|| {
Error::Parse(ParseError::InvalidUpdateBody(
"Body is required for UPDATE".to_string(),
))
})?;
let params = parse_update_params(query_string, body)?;
Ok(Operation::Update(params, prefer))
}
"DELETE" => {
let params = parse_delete_params(query_string)?;
Ok(Operation::Delete(params, prefer))
}
_ => Err(Error::Parse(ParseError::UnsupportedMethod(format!(
"Unsupported HTTP method: {}",
method
)))),
}
}
pub fn operation_to_sql(table: &str, operation: &Operation) -> Result<QueryResult, Error> {
#[cfg(any(feature = "postgres", feature = "wasm"))]
{
operation_to_sql_with_cache(table, operation, None)
}
#[cfg(not(any(feature = "postgres", feature = "wasm")))]
{
operation_to_sql_inner(table, operation, QueryBuilder::new)
}
}
#[cfg(any(feature = "postgres", feature = "wasm"))]
pub fn operation_to_sql_with_cache(
table: &str,
operation: &Operation,
schema_cache: Option<std::sync::Arc<schema_cache::SchemaCache>>,
) -> Result<QueryResult, Error> {
let make_builder = move || -> QueryBuilder {
let mut builder = QueryBuilder::new();
if let Some(cache) = &schema_cache {
builder = builder.with_schema_cache(cache.clone());
}
builder
};
operation_to_sql_inner(table, operation, make_builder)
}
fn operation_to_sql_inner(
table: &str,
operation: &Operation,
make_builder: impl Fn() -> QueryBuilder,
) -> Result<QueryResult, Error> {
match operation {
Operation::Select(params, _prefer) => {
if table.is_empty() {
return Err(Error::Sql(SqlError::EmptyTableName));
}
let mut builder = make_builder();
builder.build_select(table, params).map_err(Error::Sql)
}
Operation::Insert(params, _prefer) => {
let resolved_table = resolve_schema(table, "POST", None)?;
let mut builder = make_builder();
builder
.build_insert(&resolved_table, params)
.map_err(Error::Sql)
}
Operation::Update(params, _prefer) => {
let resolved_table = resolve_schema(table, "PATCH", None)?;
let mut builder = make_builder();
builder
.build_update(&resolved_table, params)
.map_err(Error::Sql)
}
Operation::Delete(params, _prefer) => {
let resolved_table = resolve_schema(table, "DELETE", None)?;
let mut builder = make_builder();
builder
.build_delete(&resolved_table, params)
.map_err(Error::Sql)
}
Operation::Rpc(params, _prefer) => {
let function_name = table.strip_prefix("rpc/").unwrap_or(table);
let resolved_table = resolve_schema(function_name, "POST", None)?;
let mut builder = make_builder();
builder
.build_rpc(&resolved_table, params)
.map_err(Error::Sql)
}
}
}
fn extract_conflict_columns_from_query(query_string: &str) -> Vec<String> {
if query_string.is_empty() {
return Vec::new();
}
let mut columns = Vec::new();
for pair in query_string.split('&') {
let parts: Vec<&str> = pair.splitn(2, '=').collect();
if parts.len() == 2 {
let key = parts[0];
if !parser::filter::reserved_key(key) && !parser::logic::logic_key(key) {
let column_name = if let Some(arrow_pos) = key.find("->") {
&key[..arrow_pos]
} else {
key
};
if !columns.contains(&column_name.to_string()) {
columns.push(column_name.to_string());
}
}
}
}
columns
}
fn parse_filters_from_map(
params: &std::collections::HashMap<String, String>,
) -> Result<Vec<LogicCondition>, Error> {
let mut filters = Vec::new();
for (key, value) in params {
if parser::filter::reserved_key(key) {
continue;
}
if parser::logic::logic_key(key) {
let tree = parse_logic(key, value)?;
filters.push(LogicCondition::Logic(tree));
} else {
let filter = parse_filter(key, value)?;
filters.push(LogicCondition::Filter(filter));
}
}
Ok(filters)
}
fn parse_filters_from_pairs(pairs: &[(String, String)]) -> Result<Vec<LogicCondition>, Error> {
let mut filters = Vec::new();
for (key, value) in pairs {
if parser::filter::reserved_key(key) {
continue;
}
if parser::logic::logic_key(key) {
let tree = parse_logic(key, value)?;
filters.push(LogicCondition::Logic(tree));
} else {
let filter = parse_filter(key, value)?;
filters.push(LogicCondition::Filter(filter));
}
}
Ok(filters)
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FilterClauseResult {
pub clause: String,
pub params: Vec<serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_query_string_empty() {
let result = parse_query_string("");
assert!(result.is_ok());
let params = result.unwrap();
assert!(params.is_empty());
}
#[test]
fn test_parse_query_string_simple() {
let result = parse_query_string("select=id,name&id=eq.1");
assert!(result.is_ok());
let params = result.unwrap();
assert!(params.has_select());
assert!(params.has_filters());
}
#[test]
fn test_parse_query_string_with_order() {
let result = parse_query_string("select=id&order=id.desc");
assert!(result.is_ok());
let params = result.unwrap();
assert!(params.has_select());
assert!(!params.order.is_empty());
}
#[test]
fn test_parse_query_string_with_limit() {
let result = parse_query_string("select=id&limit=10");
assert!(result.is_ok());
let params = result.unwrap();
assert_eq!(params.limit, Some(10));
}
#[test]
fn test_to_sql_simple() {
let params = ParsedParams::new()
.with_select(vec![SelectItem::field("id"), SelectItem::field("name")]);
let result = to_sql("users", ¶ms);
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("SELECT"));
assert!(query.query.contains("users"));
}
#[test]
fn test_query_string_to_sql() {
let result = query_string_to_sql("users", "select=id,name");
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("SELECT"));
assert!(query.query.contains("users"));
assert_eq!(query.tables, vec!["users"]);
}
#[test]
fn test_build_filter_clause() {
let filter = LogicCondition::Filter(Filter::new(
Field::new("id"),
FilterOperator::Eq,
FilterValue::Single("1".to_string()),
));
let result = build_filter_clause(&[filter]);
assert!(result.is_ok());
let clause = result.unwrap();
assert!(clause.clause.contains("\"id\""));
assert!(clause.clause.contains("="));
}
#[test]
fn test_complex_query_with_multiple_filters() {
let query_str = "select=id,name,email&age=gte.18&status=in.(active,pending)&order=created_at.desc&limit=10";
let result = parse_query_string(query_str);
assert!(result.is_ok());
let params = result.unwrap();
assert!(params.has_select());
assert!(params.has_filters());
assert_eq!(params.filters.len(), 2);
assert_eq!(params.order.len(), 1);
assert_eq!(params.limit, Some(10));
}
#[test]
fn test_query_with_logic_operators() {
let query_str = "and=(age.gte.18,status.eq.active)";
let result = parse_query_string(query_str);
assert!(result.is_ok());
let params = result.unwrap();
assert!(params.has_filters());
}
#[test]
fn test_query_with_json_path() {
let query_str = "data->name=eq.John&data->age=gt.25";
let result = parse_query_string(query_str);
assert!(result.is_ok());
let params = result.unwrap();
assert_eq!(params.filters.len(), 2);
}
#[test]
fn test_query_with_type_cast() {
let query_str = "price::numeric=gt.100";
let result = parse_query_string(query_str);
assert!(result.is_ok());
let params = result.unwrap();
assert_eq!(params.filters.len(), 1);
}
#[test]
fn test_query_to_sql_with_comparison_operators() {
let query_str = "age=gte.18&price=lte.100";
let result = query_string_to_sql("users", query_str);
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains(">="));
assert!(query.query.contains("<="));
assert_eq!(query.params.len(), 2);
}
#[test]
fn test_multiple_filters_same_column() {
let query_str = "price=gte.50&price=lte.150";
let params = parse_query_string(query_str).unwrap();
assert_eq!(params.filters.len(), 2, "Should have both filters");
let result = query_string_to_sql("products", query_str).unwrap();
assert!(result.query.contains(">="), "Should have >= operator");
assert!(result.query.contains("<="), "Should have <= operator");
assert_eq!(result.params.len(), 2, "Should have 2 parameter values");
assert!(result.query.contains("WHERE"));
assert!(result.query.contains("AND") || result.query.matches("price").count() == 2);
}
#[test]
fn test_query_to_sql_with_fts() {
let query_str = "content=fts(english).search term";
let result = query_string_to_sql("articles", query_str);
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("to_tsvector"));
assert!(query.query.contains("plainto_tsquery"));
assert!(query.query.contains("english"));
}
#[test]
fn test_query_to_sql_with_array_operators() {
let query_str = "tags=cs.{rust}";
let result = query_string_to_sql("posts", query_str);
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("@>"));
}
#[test]
fn test_query_to_sql_with_negation() {
let query_str = "status=not.eq.deleted";
let result = query_string_to_sql("users", query_str);
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("<>"));
}
#[test]
fn test_complex_nested_query() {
let query_str = "select=id,name,orders(id,total)&status=eq.active&age=gte.18&order=created_at.desc&limit=10&offset=20";
let result = parse_query_string(query_str);
assert!(result.is_ok());
let params = result.unwrap();
assert!(params.has_select());
assert_eq!(params.filters.len(), 2);
assert_eq!(params.order.len(), 1);
assert_eq!(params.limit, Some(10));
assert_eq!(params.offset, Some(20));
}
#[test]
fn test_query_with_quantifiers() {
let query_str = "tags=eq(any).{rust,elixir,go}";
let result = query_string_to_sql("posts", query_str);
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("= ANY"));
}
#[test]
fn test_insert_with_return_representation() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=representation".to_string());
let body = r#"{"email": "alice@example.com", "name": "Alice"}"#;
let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
match op {
Operation::Insert(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
}
_ => panic!("Expected Insert operation with Prefer"),
}
}
#[test]
fn test_insert_with_minimal_return() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=minimal".to_string());
let body = r#"[{"name": "Alice"}, {"name": "Bob"}]"#;
let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
match op {
Operation::Insert(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Minimal)
);
}
_ => panic!("Expected Insert with minimal return"),
}
}
#[test]
fn test_upsert_with_merge_duplicates() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert(
"Prefer".to_string(),
"resolution=merge-duplicates".to_string(),
);
let body = r#"{"user_id": 123, "theme": "dark"}"#;
let op = parse(
"POST",
"preferences",
"on_conflict=user_id",
Some(body),
Some(&headers),
)
.unwrap();
match op {
Operation::Insert(params, Some(prefer)) => {
assert_eq!(prefer.resolution, Some(Resolution::MergeDuplicates));
assert!(params.on_conflict.is_some());
}
_ => panic!("Expected Insert with resolution preference"),
}
}
#[test]
fn test_select_with_count_exact() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "count=exact".to_string());
let op = parse("GET", "users", "limit=10&offset=0", None, Some(&headers)).unwrap();
match op {
Operation::Select(_, Some(prefer)) => {
assert_eq!(prefer.count, Some(Count::Exact));
}
_ => panic!("Expected Select with count"),
}
}
#[test]
fn test_multiple_prefer_options() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert(
"Prefer".to_string(),
"return=representation, missing=default, plurality=singular".to_string(),
);
let body = r#"{"name": "Bob"}"#;
let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
match op {
Operation::Insert(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
assert_eq!(prefer.missing, Some(Missing::Default));
assert_eq!(prefer.plurality, Some(Plurality::Singular));
}
_ => panic!("Expected Insert with multiple preferences"),
}
}
#[test]
fn test_update_with_prefer_headers() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=representation".to_string());
let body = r#"{"status": "active"}"#;
let op = parse("PATCH", "users", "id=eq.123", Some(body), Some(&headers)).unwrap();
match op {
Operation::Update(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
}
_ => panic!("Expected Update with Prefer"),
}
}
#[test]
fn test_delete_with_prefer_headers() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=headers-only".to_string());
let op = parse("DELETE", "users", "status=eq.deleted", None, Some(&headers)).unwrap();
match op {
Operation::Delete(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::HeadersOnly)
);
}
_ => panic!("Expected Delete with Prefer"),
}
}
#[test]
fn test_prefer_header_case_insensitive() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("prefer".to_string(), "count=exact".to_string());
let op = parse("GET", "users", "", None, Some(&headers)).unwrap();
match op {
Operation::Select(_, Some(prefer)) => {
assert_eq!(prefer.count, Some(Count::Exact));
}
_ => panic!("Expected Select with Prefer"),
}
}
#[test]
fn test_no_prefer_headers() {
let op = parse("GET", "users", "id=eq.123", None, None).unwrap();
match op {
Operation::Select(_, prefer) => {
assert!(prefer.is_none());
}
_ => panic!("Expected Select without Prefer"),
}
}
#[test]
fn test_prefer_with_schema_headers() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=representation".to_string());
headers.insert("Content-Profile".to_string(), "auth".to_string());
let body = r#"{"email": "alice@example.com"}"#;
let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
match op {
Operation::Insert(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
}
_ => panic!("Expected Insert with both Prefer and schema headers"),
}
}
#[test]
fn test_rpc_post_with_args() {
let body = r#"{"user_id": 123, "status": "active"}"#;
let op = parse("POST", "rpc/get_user_posts", "", Some(body), None).unwrap();
match op {
Operation::Rpc(params, prefer) => {
assert_eq!(params.function_name, "get_user_posts");
assert_eq!(params.args.len(), 2);
assert!(prefer.is_none());
}
_ => panic!("Expected RPC operation"),
}
}
#[test]
fn test_rpc_get_no_args() {
let op = parse("GET", "rpc/health_check", "", None, None).unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "health_check");
assert!(params.args.is_empty());
}
_ => panic!("Expected RPC operation"),
}
}
#[test]
fn test_rpc_with_filters() {
let body = r#"{"department_id": 5}"#;
let query = "age=gte.25&salary=lt.100000";
let op = parse("POST", "rpc/find_employees", query, Some(body), None).unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "find_employees");
assert_eq!(params.filters.len(), 2);
}
_ => panic!("Expected RPC operation"),
}
}
#[test]
fn test_rpc_with_order_limit() {
let query = "order=created_at.desc&limit=10&offset=20";
let op = parse("GET", "rpc/list_recent_posts", query, None, None).unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "list_recent_posts");
assert_eq!(params.order.len(), 1);
assert_eq!(params.limit, Some(10));
assert_eq!(params.offset, Some(20));
}
_ => panic!("Expected RPC operation"),
}
}
#[test]
fn test_rpc_with_select() {
let body = r#"{"search_term": "laptop"}"#;
let query = "select=id,name,price";
let op = parse("POST", "rpc/search_products", query, Some(body), None).unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "search_products");
assert!(params.returning.is_some());
assert_eq!(params.returning.unwrap().len(), 3);
}
_ => panic!("Expected RPC operation"),
}
}
#[test]
fn test_rpc_with_prefer_headers() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=representation".to_string());
let body = r#"{"amount": 100.50}"#;
let op = parse(
"POST",
"rpc/process_payment",
"",
Some(body),
Some(&headers),
)
.unwrap();
match op {
Operation::Rpc(params, Some(prefer)) => {
assert_eq!(params.function_name, "process_payment");
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
}
_ => panic!("Expected RPC operation with Prefer header"),
}
}
#[test]
fn test_rpc_to_sql_simple() {
let body = r#"{"user_id": 42}"#;
let op = parse("POST", "rpc/get_profile", "", Some(body), None).unwrap();
let result = operation_to_sql("rpc/get_profile", &op).unwrap();
assert!(result.query.contains(r#"FROM "public"."get_profile"("#));
assert!(result.query.contains(r#""user_id" := $1"#));
assert_eq!(result.params.len(), 1);
}
#[test]
fn test_rpc_to_sql_with_schema() {
let body = r#"{"query": "test"}"#;
let op = parse("POST", "rpc/api.search", "", Some(body), None).unwrap();
let result = operation_to_sql("rpc/api.search", &op).unwrap();
assert!(result.query.contains(r#"FROM "api"."search"("#));
}
#[test]
fn test_rpc_to_sql_complex() {
let body = r#"{"min_price": 100, "max_price": 1000}"#;
let query = "category=eq.electronics&in_stock=eq.true&order=price.asc&limit=20&select=id,name,price";
let op = parse("POST", "rpc/find_products", query, Some(body), None).unwrap();
let result = operation_to_sql("rpc/find_products", &op).unwrap();
assert!(result.query.contains(r#"FROM "public"."find_products"("#));
assert!(result.query.contains(r#""max_price" := $1"#));
assert!(result.query.contains(r#""min_price" := $2"#));
assert!(result.query.contains("WHERE"));
assert!(result.query.contains("ORDER BY"));
assert!(result.query.contains("LIMIT"));
assert!(result.params.len() > 2);
}
#[test]
fn test_rpc_invalid_empty_function_name() {
let result = parse("POST", "rpc/", "", None, None);
assert!(result.is_err());
}
#[test]
fn test_rpc_get_with_query_params() {
let query = "limit=5";
let op = parse("GET", "rpc/get_stats", query, None, None).unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "get_stats");
assert_eq!(params.limit, Some(5));
}
_ => panic!("Expected RPC operation"),
}
}
#[test]
fn test_insert_with_select_parameter() {
let body = r#"{"email": "bob@example.com", "name": "Bob"}"#;
let query = "select=id,email,created_at";
let op = parse("POST", "users", query, Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.returning.is_some());
let returning = params.returning.unwrap();
assert_eq!(returning.len(), 3);
assert_eq!(returning[0].name, "id");
assert_eq!(returning[1].name, "email");
assert_eq!(returning[2].name, "created_at");
}
_ => panic!("Expected Insert with select"),
}
}
#[test]
fn test_update_with_select_parameter() {
let body = r#"{"status": "verified"}"#;
let query = "id=eq.123&select=id,status,updated_at";
let op = parse("PATCH", "users", query, Some(body), None).unwrap();
match op {
Operation::Update(params, _) => {
assert!(params.returning.is_some());
let returning = params.returning.unwrap();
assert_eq!(returning.len(), 3);
}
_ => panic!("Expected Update with select"),
}
}
#[test]
fn test_delete_with_select_parameter() {
let query = "status=eq.inactive&select=id,email";
let op = parse("DELETE", "users", query, None, None).unwrap();
match op {
Operation::Delete(params, _) => {
assert!(params.returning.is_some());
let returning = params.returning.unwrap();
assert_eq!(returning.len(), 2);
}
_ => panic!("Expected Delete with select"),
}
}
#[test]
fn test_insert_with_returning_backwards_compat() {
let body = r#"{"email": "alice@example.com"}"#;
let query = "returning=id,created_at";
let op = parse("POST", "users", query, Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.returning.is_some());
assert_eq!(params.returning.unwrap().len(), 2);
}
_ => panic!("Expected Insert with returning"),
}
}
#[test]
fn test_select_takes_precedence_over_returning() {
let body = r#"{"email": "test@example.com"}"#;
let query = "select=id&returning=id,email,name";
let op = parse("POST", "users", query, Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.returning.is_some());
let returning = params.returning.unwrap();
assert_eq!(returning.len(), 1);
assert_eq!(returning[0].name, "id");
}
_ => panic!("Expected Insert"),
}
}
#[test]
fn test_mutation_select_to_sql() {
let body = r#"{"name": "New Product", "price": 99.99}"#;
let query = "select=id,name,created_at";
let op = parse("POST", "products", query, Some(body), None).unwrap();
let result = operation_to_sql("products", &op).unwrap();
assert!(result.query.contains("RETURNING"));
assert!(result.query.contains(r#""id""#));
assert!(result.query.contains(r#""name""#));
assert!(result.query.contains(r#""created_at""#));
}
#[test]
fn test_put_upsert_basic() {
let body = r#"{"email": "alice@example.com", "name": "Alice Updated"}"#;
let query = "email=eq.alice@example.com";
let op = parse("PUT", "users", query, Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.on_conflict.is_some());
let conflict = params.on_conflict.unwrap();
assert_eq!(conflict.columns, vec!["email"]);
assert_eq!(conflict.action, ConflictAction::DoUpdate);
}
_ => panic!("Expected Insert (upsert) operation"),
}
}
#[test]
fn test_put_upsert_multiple_columns() {
let body = r#"{"email": "bob@example.com", "team": "engineering", "role": "senior"}"#;
let query = "email=eq.bob@example.com&team=eq.engineering";
let op = parse("PUT", "users", query, Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.on_conflict.is_some());
let conflict = params.on_conflict.unwrap();
assert_eq!(conflict.columns.len(), 2);
assert!(conflict.columns.contains(&"email".to_string()));
assert!(conflict.columns.contains(&"team".to_string()));
}
_ => panic!("Expected Insert with multi-column conflict"),
}
}
#[test]
fn test_put_with_explicit_on_conflict() {
let body = r#"{"id": 123, "name": "Test"}"#;
let query = "id=eq.123&on_conflict=id";
let op = parse("PUT", "items", query, Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.on_conflict.is_some());
let conflict = params.on_conflict.unwrap();
assert_eq!(conflict.columns, vec!["id"]);
}
_ => panic!("Expected Insert"),
}
}
#[test]
fn test_put_without_filters() {
let body = r#"{"name": "New Item"}"#;
let op = parse("PUT", "items", "", Some(body), None).unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.on_conflict.is_none());
}
_ => panic!("Expected Insert"),
}
}
#[test]
fn test_put_to_sql() {
let body = r#"{"email": "test@example.com", "name": "Test User"}"#;
let query = "email=eq.test@example.com&select=id,email,name";
let op = parse("PUT", "users", query, Some(body), None).unwrap();
let result = operation_to_sql("users", &op).unwrap();
assert!(result.query.contains("INSERT INTO"));
assert!(result.query.contains("ON CONFLICT"));
assert!(result.query.contains("DO UPDATE SET"));
assert!(result.query.contains("RETURNING"));
}
#[test]
fn test_put_requires_body() {
let result = parse("PUT", "users", "id=eq.123", None, None);
assert!(result.is_err());
}
#[test]
fn test_on_conflict_with_where_clause() {
use crate::parser::parse_filter;
let body = r#"{"email": "alice@example.com", "name": "Alice"}"#;
let mut params = parse_insert_params("", body).unwrap();
let filter = parse_filter("deleted_at", "is.null").unwrap();
let conflict = OnConflict::do_update(vec!["email".to_string()])
.with_where_clause(vec![LogicCondition::Filter(filter)]);
params = params.with_on_conflict(conflict);
let op = Operation::Insert(params, None);
let result = operation_to_sql("users", &op).unwrap();
assert!(result.query.contains("ON CONFLICT"));
assert!(result.query.contains(r#"("email")"#));
assert!(result.query.contains("WHERE"));
assert!(result.query.contains("deleted_at"));
}
#[test]
fn test_on_conflict_with_specific_update_columns() {
let body = r#"{"email": "bob@example.com", "name": "Bob", "role": "admin"}"#;
let mut params = parse_insert_params("", body).unwrap();
let conflict = OnConflict::do_update(vec!["email".to_string()])
.with_update_columns(vec!["name".to_string()]);
params = params.with_on_conflict(conflict);
let op = Operation::Insert(params, None);
let result = operation_to_sql("users", &op).unwrap();
assert!(result.query.contains("ON CONFLICT"));
assert!(result.query.contains(r#""name" = EXCLUDED."name""#));
assert!(!result.query.contains(r#""role" = EXCLUDED."role""#));
}
#[test]
fn test_on_conflict_complex() {
use crate::parser::parse_filter;
let body = r#"{"user_id": 123, "post_id": 456, "reaction": "like"}"#;
let mut params = parse_insert_params("", body).unwrap();
let filter = parse_filter("deleted_at", "is.null").unwrap();
let conflict = OnConflict::do_update(vec!["user_id".to_string(), "post_id".to_string()])
.with_where_clause(vec![LogicCondition::Filter(filter)])
.with_update_columns(vec!["reaction".to_string()]);
params = params.with_on_conflict(conflict);
let op = Operation::Insert(params, None);
let result = operation_to_sql("reactions", &op).unwrap();
println!("SQL: {}", result.query);
assert!(
result
.query
.contains(r#"ON CONFLICT ("post_id", "user_id")"#)
|| result
.query
.contains(r#"ON CONFLICT ("user_id", "post_id")"#)
);
assert!(result.query.contains("WHERE"));
assert!(result.query.contains(r#""reaction" = EXCLUDED."reaction""#));
}
#[test]
fn test_ecommerce_workflow() {
use std::collections::HashMap;
let body = r#"[
{"product_id": 1, "quantity": 2, "price": 29.99},
{"product_id": 3, "quantity": 1, "price": 49.99}
]"#;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=representation".to_string());
headers.insert("Content-Profile".to_string(), "sales".to_string());
let op = parse(
"POST",
"order_items",
"select=*",
Some(body),
Some(&headers),
)
.unwrap();
match op {
Operation::Insert(params, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
assert!(params.returning.is_some());
}
_ => panic!("Expected Insert with Prefer"),
}
let body = r#"{"status": "shipped", "shipped_at": "2024-01-15"}"#;
let op = parse(
"PATCH",
"orders",
"id=eq.123&select=id,status,shipped_at",
Some(body),
None,
)
.unwrap();
match op {
Operation::Update(params, _) => {
assert!(params.has_filters());
assert!(params.returning.is_some());
}
_ => panic!("Expected Update"),
}
let body = r#"{"order_id": 123}"#;
let op = parse("POST", "rpc/calculate_order_total", "", Some(body), None).unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "calculate_order_total");
}
_ => panic!("Expected RPC"),
}
}
#[test]
fn test_social_media_workflow() {
use std::collections::HashMap;
let body = r#"{"content": "Hello World!", "user_id": 456}"#;
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "return=representation".to_string());
let op = parse(
"POST",
"posts",
"select=id,content,user_id",
Some(body),
Some(&headers),
)
.unwrap();
match op {
Operation::Insert(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
}
_ => panic!("Expected Insert"),
}
let body = r#"{"user_id": 789, "post_id": 123}"#;
let op = parse(
"PUT",
"likes",
"user_id=eq.789&post_id=eq.123",
Some(body),
None,
)
.unwrap();
match op {
Operation::Insert(params, _) => {
assert!(params.on_conflict.is_some());
}
_ => panic!("Expected upsert"),
}
let op = parse(
"DELETE",
"posts",
"created_at=lt.2020-01-01&order=created_at.asc&limit=100",
None,
None,
)
.unwrap();
match op {
Operation::Delete(params, _) => {
assert!(params.has_filters());
assert_eq!(params.limit, Some(100));
}
_ => panic!("Expected Delete"),
}
}
#[test]
fn test_analytics_workflow() {
use std::collections::HashMap;
let body = r#"[
{"metric": "pageviews", "value": 1234, "date": "2024-01-15"},
{"metric": "signups", "value": 56, "date": "2024-01-15"}
]"#;
let mut headers = HashMap::new();
headers.insert(
"Prefer".to_string(),
"resolution=merge-duplicates".to_string(),
);
let op = parse(
"POST",
"metrics",
"on_conflict=metric,date",
Some(body),
Some(&headers),
)
.unwrap();
match op {
Operation::Insert(params, Some(prefer)) => {
assert!(params.on_conflict.is_some());
assert_eq!(prefer.resolution, Some(Resolution::MergeDuplicates));
}
_ => panic!("Expected Insert with resolution"),
}
let body = r#"{"start_date": "2024-01-01", "end_date": "2024-01-31"}"#;
let op = parse(
"POST",
"rpc/get_monthly_stats",
"metric=eq.pageviews",
Some(body),
None,
)
.unwrap();
match op {
Operation::Rpc(params, _) => {
assert_eq!(params.function_name, "get_monthly_stats");
assert!(!params.filters.is_empty());
}
_ => panic!("Expected RPC with filters"),
}
let mut headers = HashMap::new();
headers.insert("Prefer".to_string(), "count=exact".to_string());
let op = parse(
"GET",
"events",
"created_at=gte.2024-01-01",
None,
Some(&headers),
)
.unwrap();
match op {
Operation::Select(_, Some(prefer)) => {
assert_eq!(prefer.count, Some(Count::Exact));
}
_ => panic!("Expected Select with count"),
}
}
#[test]
fn test_embedding_many_to_one_via_fk() {
let result = query_string_to_sql("posts", "select=*,profiles(username,avatar_url)");
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("SELECT"));
assert!(query.query.contains("profiles"));
assert!(
!query.query.contains("row_to_json(profiles.\"username\""),
"row_to_json must not receive individual columns: {}",
query.query
);
assert!(
query.query.contains("row_to_json("),
"should use row_to_json with a subquery record: {}",
query.query
);
}
#[test]
fn test_embedding_one_to_many() {
let result = query_string_to_sql("posts", "select=title,comments(id,body)");
assert!(result.is_ok());
let query = result.unwrap();
assert!(query.query.contains("\"title\""));
assert!(query.query.contains("comments"));
assert!(
!query.query.contains("row_to_json(comments.\"id\""),
"row_to_json must not receive individual columns: {}",
query.query
);
}
#[test]
fn test_embedding_select_star_produces_valid_row_to_json() {
let result = query_string_to_sql("posts", "select=*,comments(*)");
assert!(result.is_ok());
let query = result.unwrap();
assert!(
query.query.contains("row_to_json("),
"should use row_to_json: {}",
query.query
);
}
#[test]
fn test_embedding_nested_produces_valid_sql() {
let result = query_string_to_sql(
"posts",
"select=id,comments(id,body,author:profiles(name,avatar_url))",
);
assert!(result.is_ok());
let query = result.unwrap();
assert!(
!query.query.contains("row_to_json(profiles.\"name\""),
"nested row_to_json must not receive individual columns: {}",
query.query
);
}
#[test]
fn test_embedding_aliased_relation() {
let params = parse_query_string("select=*,author:profiles(name)").unwrap();
let select = params.select.as_ref().unwrap();
let relation = &select[1];
assert_eq!(relation.name, "profiles");
assert_eq!(relation.alias, Some("author".to_string()));
assert_eq!(relation.item_type, ItemType::Relation);
}
#[test]
fn test_embedding_nested_with_alias() {
let params = parse_query_string("select=*,comments(id,author:profiles(name))").unwrap();
let select = params.select.as_ref().unwrap();
let comments = &select[1];
assert_eq!(comments.name, "comments");
let children = comments.children.as_ref().unwrap();
assert_eq!(children[1].name, "profiles");
assert_eq!(children[1].alias, Some("author".to_string()));
assert_eq!(children[1].item_type, ItemType::Relation);
let nested = children[1].children.as_ref().unwrap();
assert_eq!(nested[0].name, "name");
}
#[test]
fn test_embedding_fk_hint_disambiguation() {
let params = parse_query_string("select=*,author:profiles!author_id_fkey(name)").unwrap();
let select = params.select.as_ref().unwrap();
let relation = &select[1];
assert_eq!(relation.name, "profiles");
assert_eq!(relation.alias, Some("author".to_string()));
assert!(relation.hint.is_some());
assert_eq!(
relation.hint,
Some(ItemHint::Inner("author_id_fkey".to_string()))
);
}
#[test]
fn test_embedding_with_filters_and_ordering() {
let query_str = "select=id,title,author:profiles(name,avatar_url),comments(id,body)&status=eq.published&order=created_at.desc&limit=10";
let params = parse_query_string(query_str).unwrap();
assert!(params.has_select());
let select = params.select.as_ref().unwrap();
assert_eq!(select.len(), 4); assert_eq!(select[2].alias, Some("author".to_string()));
assert_eq!(select[3].name, "comments");
assert!(params.has_filters());
assert_eq!(params.order.len(), 1);
assert_eq!(params.limit, Some(10));
}
#[test]
fn test_embedding_supabase_blog_example() {
let query_str = "select=id,title,content,author:profiles!author_id_fkey(name,avatar_url),comments(id,body,created_at,commenter:profiles!commenter_id_fkey(name))&published=eq.true&order=created_at.desc&limit=20";
let params = parse_query_string(query_str).unwrap();
let select = params.select.as_ref().unwrap();
assert_eq!(select.len(), 5);
let author = &select[3];
assert_eq!(author.name, "profiles");
assert_eq!(author.alias, Some("author".to_string()));
assert_eq!(
author.hint,
Some(ItemHint::Inner("author_id_fkey".to_string()))
);
let comments = &select[4];
assert_eq!(comments.name, "comments");
let comment_children = comments.children.as_ref().unwrap();
assert_eq!(comment_children.len(), 4);
let commenter = &comment_children[3];
assert_eq!(commenter.name, "profiles");
assert_eq!(commenter.alias, Some("commenter".to_string()));
assert_eq!(
commenter.hint,
Some(ItemHint::Inner("commenter_id_fkey".to_string()))
);
}
#[test]
fn test_100_percent_parity_demonstration() {
use std::collections::HashMap;
let body = r#"{"email": "test@example.com"}"#;
assert!(parse("POST", "users", "", Some(body), None).is_ok());
assert!(parse("PUT", "users", "id=eq.1", Some(body), None).is_ok());
assert!(parse("PATCH", "users", "id=eq.1", Some(body), None).is_ok());
assert!(parse("DELETE", "users", "id=eq.1", None, None).is_ok());
assert!(parse("POST", "rpc/my_function", "", Some(body), None).is_ok());
assert!(parse("GET", "rpc/my_function", "", None, None).is_ok());
let mut headers = HashMap::new();
headers.insert(
"Prefer".to_string(),
"return=representation, count=exact, resolution=merge-duplicates, plurality=singular, missing=default".to_string(),
);
let op = parse("GET", "users", "", None, Some(&headers)).unwrap();
match op {
Operation::Select(_, Some(prefer)) => {
assert_eq!(
prefer.return_representation,
Some(ReturnRepresentation::Full)
);
assert_eq!(prefer.count, Some(Count::Exact));
assert_eq!(prefer.resolution, Some(Resolution::MergeDuplicates));
assert_eq!(prefer.plurality, Some(Plurality::Singular));
assert_eq!(prefer.missing, Some(Missing::Default));
}
_ => panic!("Expected all prefer options"),
}
let mut headers = HashMap::new();
headers.insert("Accept-Profile".to_string(), "api".to_string());
assert!(parse("GET", "users", "", None, Some(&headers)).is_ok());
assert!(parse(
"GET",
"users",
"age=gte.18&status=in.(active,verified)&order=created_at.desc&limit=10&offset=20&select=id,name",
None,
None
)
.is_ok());
assert!(parse("POST", "users", "on_conflict=email", Some(body), None).is_ok());
println!("✅ 100% PostgREST Parity Achieved!");
}
#[cfg(any(feature = "postgres", feature = "wasm"))]
mod operation_to_sql_with_cache_tests {
use super::*;
use crate::schema_cache::ForeignKey;
#[test]
fn test_with_cache_none_matches_without_cache() {
let op = parse("GET", "users", "id=eq.1", None, None).unwrap();
let with_cache = operation_to_sql_with_cache("users", &op, None).unwrap();
let without_cache = operation_to_sql("users", &op).unwrap();
assert_eq!(with_cache.query, without_cache.query);
assert_eq!(with_cache.params, without_cache.params);
assert_eq!(with_cache.tables, without_cache.tables);
}
#[test]
fn test_with_cache_select() {
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
);
let op = parse("GET", "users", "age=gte.18&limit=5", None, None).unwrap();
let result =
operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
assert!(result.query.contains("SELECT"));
assert!(result.query.contains("WHERE"));
assert!(result.query.contains("LIMIT"));
}
#[test]
fn test_with_cache_insert() {
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
);
let body = r#"{"name":"Alice"}"#;
let op = parse("POST", "users", "", Some(body), None).unwrap();
let result =
operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
assert!(result.query.contains("INSERT"));
}
#[test]
fn test_with_cache_update() {
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
);
let body = r#"{"status":"active"}"#;
let op = parse("PATCH", "users", "id=eq.1", Some(body), None).unwrap();
let result =
operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
assert!(result.query.contains("UPDATE"));
}
#[test]
fn test_with_cache_delete() {
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
);
let op = parse("DELETE", "users", "id=eq.1", None, None).unwrap();
let result =
operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
assert!(result.query.contains("DELETE"));
}
#[test]
fn test_with_cache_rpc() {
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
);
let body = r#"{"user_id": 1}"#;
let op =
parse("POST", "rpc/get_profile", "", Some(body), None).unwrap();
let result = operation_to_sql_with_cache(
"rpc/get_profile",
&op,
Some(cache),
)
.unwrap();
assert!(result.query.contains("get_profile"));
}
#[test]
fn test_with_cache_empty_table_errors() {
let op = parse("GET", "users", "id=eq.1", None, None).unwrap();
let result = operation_to_sql_with_cache("", &op, None);
assert!(result.is_err());
}
#[test]
fn test_with_cache_resolves_many_to_one_relation() {
let fks = vec![ForeignKey::test("orders", "customer_id", "customers", "id")];
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(fks),
);
let op = parse(
"GET",
"orders",
"select=id,customers(name)",
None,
None,
)
.unwrap();
let result =
operation_to_sql_with_cache("orders", &op, Some(cache)).unwrap();
assert!(result.query.contains("customers"));
assert!(result.query.contains("customer_id"));
}
#[test]
fn test_with_cache_resolves_one_to_many_relation() {
let fks = vec![ForeignKey::test("orders", "customer_id", "customers", "id")];
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(fks),
);
let op = parse(
"GET",
"customers",
"select=id,orders(id)",
None,
None,
)
.unwrap();
let result =
operation_to_sql_with_cache("customers", &op, Some(cache))
.unwrap();
assert!(result.query.contains("orders"));
assert!(result.query.contains("json_agg"));
}
#[test]
fn test_with_cache_relation_not_found_errors() {
let cache = std::sync::Arc::new(
crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
);
let op = parse(
"GET",
"orders",
"select=id,nonexistent(name)",
None,
None,
)
.unwrap();
let result =
operation_to_sql_with_cache("orders", &op, Some(cache));
assert!(result.is_err());
}
#[test]
fn test_without_cache_relation_uses_placeholder() {
let op = parse(
"GET",
"orders",
"select=id,customers(name)",
None,
None,
)
.unwrap();
let result = operation_to_sql_with_cache("orders", &op, None).unwrap();
assert!(result.query.contains("SELECT"));
}
}
}