Skip to main content

postgrest_parser/
lib.rs

1//! # PostgREST Query Parser
2//!
3//! A high-performance Rust library for parsing PostgREST query strings into structured SQL queries.
4//!
5//! ## Features
6//!
7//! - **Complete PostgREST API Support**: All 22+ filter operators (eq, neq, gt, gte, lt, lte, like, ilike, match, imatch, in, is, fts, plfts, phfts, wfts, cs, cd, ov, sl, sr, nxl, nxr, adj)
8//! - **Logic Operators**: AND, OR, NOT with arbitrary nesting
9//! - **JSON Path Navigation**: `->` and `->>` operators for JSONB fields
10//! - **Type Casting**: Cast fields with `::type` syntax
11//! - **Full-Text Search**: Multiple FTS operators with language support
12//! - **Quantifiers**: `any` and `all` quantifiers for array comparisons
13//! - **Array/Range Operators**: PostgreSQL array and range type support
14//! - **Ordering**: Multi-column ordering with nulls handling
15//! - **Pagination**: `limit` and `offset` support
16//! - **SQL Generation**: Convert parsed queries to parameterized PostgreSQL SQL
17//!
18//! ## Quick Start
19//!
20//! ```rust
21//! use postgrest_parser::{parse_query_string, query_string_to_sql};
22//!
23//! // Parse a PostgREST query string
24//! let query = "select=id,name,email&age=gte.18&status=in.(active,pending)&order=created_at.desc&limit=10";
25//! let params = parse_query_string(query).unwrap();
26//!
27//! assert!(params.has_select());
28//! assert!(params.has_filters());
29//! assert_eq!(params.limit, Some(10));
30//!
31//! // Convert to SQL
32//! let result = query_string_to_sql("users", query).unwrap();
33//! println!("SQL: {}", result.query);
34//! println!("Params: {:?}", result.params);
35//! ```
36//!
37//! ## Filter Operators
38//!
39//! ### Comparison Operators
40//! - `eq` - Equal to
41//! - `neq` - Not equal to
42//! - `gt` - Greater than
43//! - `gte` - Greater than or equal to
44//! - `lt` - Less than
45//! - `lte` - Less than or equal to
46//!
47//! ### Pattern Matching
48//! - `like` - SQL LIKE pattern matching
49//! - `ilike` - Case-insensitive LIKE
50//! - `match` - POSIX regex match
51//! - `imatch` - Case-insensitive regex match
52//!
53//! ### Array Operators
54//! - `in` - Value in list
55//! - `cs` - Contains (array/range)
56//! - `cd` - Contained in (array/range)
57//! - `ov` - Overlaps (array)
58//!
59//! ### Full-Text Search
60//! - `fts` - Full-text search using plainto_tsquery
61//! - `plfts` - Plain full-text search (alias for fts)
62//! - `phfts` - Phrase full-text search using phraseto_tsquery
63//! - `wfts` - Websearch full-text search using websearch_to_tsquery
64//!
65//! ### Range Operators
66//! - `sl` - Strictly left of
67//! - `sr` - Strictly right of
68//! - `nxl` - Does not extend to right of
69//! - `nxr` - Does not extend to left of
70//! - `adj` - Adjacent to
71//!
72//! ### Special Operators
73//! - `is` - IS NULL, IS TRUE, IS FALSE, etc.
74//!
75//! ## Examples
76//!
77//! ### Simple Filtering
78//! ```rust
79//! use postgrest_parser::parse_query_string;
80//!
81//! let query = "age=gte.18&status=eq.active";
82//! let params = parse_query_string(query).unwrap();
83//! assert_eq!(params.filters.len(), 2);
84//! ```
85//!
86//! ### Logic Operators
87//! ```rust
88//! use postgrest_parser::parse_query_string;
89//!
90//! let query = "and=(age.gte.18,status.eq.active)";
91//! let params = parse_query_string(query).unwrap();
92//! assert!(params.has_filters());
93//! ```
94//!
95//! ### JSON Path Navigation
96//! ```rust
97//! use postgrest_parser::parse_query_string;
98//!
99//! let query = "data->name=eq.John&data->>email=like.*@example.com";
100//! let params = parse_query_string(query).unwrap();
101//! assert_eq!(params.filters.len(), 2);
102//! ```
103//!
104//! ### Full-Text Search
105//! ```rust
106//! use postgrest_parser::parse_query_string;
107//!
108//! let query = "content=fts(english).search term";
109//! let params = parse_query_string(query).unwrap();
110//! ```
111//!
112//! ### Quantifiers
113//! ```rust
114//! use postgrest_parser::parse_query_string;
115//!
116//! let query = "tags=eq(any).{rust,elixir}";
117//! let params = parse_query_string(query).unwrap();
118//! ```
119
120use serde::Serialize;
121
122pub mod ast;
123pub mod error;
124pub mod parser;
125pub mod sql;
126
127#[cfg(any(feature = "postgres", feature = "wasm"))]
128pub mod schema_cache;
129
130#[cfg(feature = "wasm")]
131pub mod wasm;
132
133pub use ast::{
134    Cardinality, Column, ConflictAction, Count, DeleteParams, Direction, Field, Filter,
135    FilterOperator, FilterValue, InsertParams, InsertValues, ItemHint, ItemType, JsonOp, Junction,
136    LogicCondition, LogicOperator, LogicTree, Missing, Nulls, OnConflict, Operation, OrderTerm,
137    ParsedParams, Plurality, PreferOptions, Quantifier, Relationship, Resolution, ResolvedTable,
138    ReturnRepresentation, RpcParams, SelectItem, Table, UpdateParams,
139};
140pub use error::{Error, ParseError, SqlError};
141pub use parser::{
142    field, get_profile_header, identifier, json_path, json_path_segment, logic_key,
143    parse_delete_params, parse_filter, parse_insert_params, parse_json_body, parse_logic,
144    parse_order, parse_order_term, parse_prefer_header, parse_qualified_table, parse_rpc_params,
145    parse_select, parse_update_params, reserved_key, resolve_schema, type_cast,
146    validate_insert_body, validate_update_body,
147};
148pub use sql::{QueryBuilder, QueryResult};
149
150#[cfg(feature = "postgres")]
151pub use schema_cache::{ForeignKey, RelationType, SchemaCache};
152
153/// Parses a PostgREST query string into structured parameters.
154///
155/// # Arguments
156///
157/// * `query_string` - A query string in PostgREST format (e.g., "select=id,name&age=gte.18")
158///
159/// # Returns
160///
161/// Returns `Ok(ParsedParams)` containing parsed select, filters, order, limit, and offset,
162/// or an `Err(Error)` if parsing fails.
163///
164/// # Examples
165///
166/// ```
167/// use postgrest_parser::parse_query_string;
168///
169/// let query = "select=id,name&age=gte.18&order=created_at.desc&limit=10";
170/// let params = parse_query_string(query).unwrap();
171///
172/// assert!(params.has_select());
173/// assert!(params.has_filters());
174/// assert_eq!(params.limit, Some(10));
175/// ```
176pub fn parse_query_string(query_string: &str) -> Result<ParsedParams, Error> {
177    let pairs: Vec<(String, String)> = query_string
178        .split('&')
179        .filter_map(|pair| {
180            let parts: Vec<&str> = pair.splitn(2, '=').collect();
181            if parts.len() == 2 {
182                Some((parts[0].to_string(), parts[1].to_string()))
183            } else {
184                None
185            }
186        })
187        .collect();
188
189    parse_params_from_pairs(pairs)
190}
191
192/// Parses query parameters from a HashMap into structured parameters.
193///
194/// This is useful when you already have parsed URL parameters (e.g., from a web framework).
195///
196/// # Arguments
197///
198/// * `params` - A HashMap containing query parameter key-value pairs
199///
200/// # Examples
201///
202/// ```
203/// use postgrest_parser::parse_params;
204/// use std::collections::HashMap;
205///
206/// let mut params = HashMap::new();
207/// params.insert("select".to_string(), "id,name".to_string());
208/// params.insert("age".to_string(), "gte.18".to_string());
209///
210/// let parsed = parse_params(&params).unwrap();
211/// assert!(parsed.has_select());
212/// assert!(parsed.has_filters());
213/// ```
214pub fn parse_params(
215    params: &std::collections::HashMap<String, String>,
216) -> Result<ParsedParams, Error> {
217    let select_str = params.get("select").map(|s| s.to_string());
218    let order_str = params.get("order").map(|s| s.to_string());
219    let filters = parse_filters_from_map(params)?;
220    let limit = params.get("limit").and_then(|s| s.parse::<u64>().ok());
221    let offset = params.get("offset").and_then(|s| s.parse::<u64>().ok());
222
223    let mut parsed = ParsedParams::new().with_filters(filters);
224
225    if let Some(select_str) = select_str {
226        parsed = parsed.with_select(parse_select(&select_str)?);
227    }
228
229    if let Some(order_str) = order_str {
230        parsed = parsed.with_order(parse_order(&order_str)?);
231    }
232
233    if let Some(lim) = limit {
234        parsed = parsed.with_limit(lim);
235    }
236
237    if let Some(off) = offset {
238        parsed = parsed.with_offset(off);
239    }
240
241    Ok(parsed)
242}
243
244pub fn parse_params_from_pairs(pairs: Vec<(String, String)>) -> Result<ParsedParams, Error> {
245    // Build a HashMap for single-value keys (select, order, limit, offset)
246    // But keep ALL filter pairs to support multiple filters on same column
247    let mut single_value_map = std::collections::HashMap::new();
248    let mut filter_pairs = Vec::new();
249
250    for (key, value) in pairs {
251        if parser::filter::reserved_key(&key) {
252            // Reserved keys: select, order, limit, offset - only keep last value
253            single_value_map.insert(key, value);
254        } else {
255            // Filter keys: keep all pairs to support multiple filters on same column
256            filter_pairs.push((key, value));
257        }
258    }
259
260    // Parse single-value parameters
261    let select_str = single_value_map.get("select").map(|s| s.to_string());
262    let order_str = single_value_map.get("order").map(|s| s.to_string());
263    let limit = single_value_map
264        .get("limit")
265        .and_then(|s| s.parse::<u64>().ok());
266    let offset = single_value_map
267        .get("offset")
268        .and_then(|s| s.parse::<u64>().ok());
269
270    // Parse filters from pairs (supports multiple filters on same column)
271    let filters = parse_filters_from_pairs(&filter_pairs)?;
272
273    let mut parsed = ParsedParams::new().with_filters(filters);
274
275    if let Some(select_str) = select_str {
276        parsed = parsed.with_select(parse_select(&select_str)?);
277    }
278
279    if let Some(order_str) = order_str {
280        parsed = parsed.with_order(parse_order(&order_str)?);
281    }
282
283    if let Some(lim) = limit {
284        parsed = parsed.with_limit(lim);
285    }
286
287    if let Some(off) = offset {
288        parsed = parsed.with_offset(off);
289    }
290
291    Ok(parsed)
292}
293
294/// Converts parsed parameters into a parameterized PostgreSQL SELECT query.
295///
296/// # Arguments
297///
298/// * `table` - The table name to query
299/// * `params` - Parsed parameters containing select, filters, order, limit, and offset
300///
301/// # Returns
302///
303/// Returns a `QueryResult` containing the SQL query string and parameter values.
304///
305/// # Examples
306///
307/// ```
308/// use postgrest_parser::{parse_query_string, to_sql};
309///
310/// let params = parse_query_string("age=gte.18&order=name.asc&limit=10").unwrap();
311/// let result = to_sql("users", &params).unwrap();
312///
313/// assert!(result.query.contains("SELECT"));
314/// assert!(result.query.contains("WHERE"));
315/// assert!(result.query.contains("ORDER BY"));
316/// assert!(result.query.contains("LIMIT"));
317/// ```
318pub fn to_sql(table: &str, params: &ParsedParams) -> Result<QueryResult, Error> {
319    if table.is_empty() {
320        return Err(Error::Sql(SqlError::EmptyTableName));
321    }
322
323    let mut builder = QueryBuilder::new();
324    builder.build_select(table, params).map_err(Error::Sql)
325}
326
327/// Parses a PostgREST query string and converts it directly to SQL.
328///
329/// This is a convenience function that combines `parse_query_string` and `to_sql`.
330///
331/// # Arguments
332///
333/// * `table` - The table name to query
334/// * `query_string` - A PostgREST query string
335///
336/// # Returns
337///
338/// Returns a `QueryResult` containing the SQL query and parameters.
339///
340/// # Examples
341///
342/// ```
343/// use postgrest_parser::query_string_to_sql;
344///
345/// let result = query_string_to_sql(
346///     "users",
347///     "select=id,name,email&age=gte.18&status=eq.active"
348/// ).unwrap();
349///
350/// assert!(result.query.contains("SELECT"));
351/// assert!(result.query.contains("\"id\""));
352/// assert_eq!(result.tables, vec!["users"]);
353/// ```
354pub fn query_string_to_sql(table: &str, query_string: &str) -> Result<QueryResult, Error> {
355    let params = parse_query_string(query_string)?;
356    to_sql(table, &params)
357}
358
359/// Builds a WHERE clause from filter conditions without the full query.
360///
361/// Useful when you need just the filter clause portion of a query.
362///
363/// # Arguments
364///
365/// * `filters` - A slice of logic conditions (filters)
366///
367/// # Returns
368///
369/// Returns a `FilterClauseResult` containing the WHERE clause and parameters.
370///
371/// # Examples
372///
373/// ```
374/// use postgrest_parser::{build_filter_clause, Filter, Field, FilterOperator, FilterValue, LogicCondition};
375///
376/// let filter = LogicCondition::Filter(Filter::new(
377///     Field::new("age"),
378///     FilterOperator::Gte,
379///     FilterValue::Single("18".to_string()),
380/// ));
381///
382/// let result = build_filter_clause(&[filter]).unwrap();
383/// assert!(result.clause.contains("\"age\""));
384/// assert!(result.clause.contains(">="));
385/// ```
386pub fn build_filter_clause(filters: &[LogicCondition]) -> Result<FilterClauseResult, Error> {
387    let mut builder = QueryBuilder::new();
388    builder.build_where_clause(filters).map_err(Error::Sql)?;
389
390    Ok(FilterClauseResult {
391        clause: builder.sql.clone(),
392        params: builder.params.clone(),
393    })
394}
395
396/// Unified parse function for all PostgREST operations
397///
398/// # Arguments
399///
400/// * `method` - HTTP method (GET, POST, PATCH, DELETE)
401/// * `table` - Table name, optionally schema-qualified (e.g., "users" or "auth.users")
402/// * `query_string` - Query parameters
403/// * `body` - Optional JSON body for mutations
404/// * `headers` - Optional headers for schema resolution
405///
406/// # Examples
407///
408/// ```
409/// use postgrest_parser::parse;
410/// use std::collections::HashMap;
411///
412/// // SELECT
413/// let op = parse("GET", "users", "id=eq.123", None, None).unwrap();
414///
415/// // INSERT
416/// let body = r#"{"name": "Alice"}"#;
417/// let op = parse("POST", "users", "", Some(body), None).unwrap();
418///
419/// // UPDATE with schema
420/// let mut headers = HashMap::new();
421/// headers.insert("Content-Profile".to_string(), "auth".to_string());
422/// let body = r#"{"status": "active"}"#;
423/// let op = parse("PATCH", "users", "id=eq.123", Some(body), Some(&headers)).unwrap();
424/// ```
425pub fn parse(
426    method: &str,
427    table: &str,
428    query_string: &str,
429    body: Option<&str>,
430    headers: Option<&std::collections::HashMap<String, String>>,
431) -> Result<Operation, Error> {
432    // Check if this is an RPC call (table starts with "rpc/")
433    if let Some(function_name) = table.strip_prefix("rpc/") {
434        // Validate function name
435        if function_name.is_empty() {
436            return Err(Error::Parse(ParseError::InvalidTableName(
437                "RPC function name cannot be empty".to_string(),
438            )));
439        }
440
441        // Validate schema for RPC
442        let _resolved_table = resolve_schema(function_name, method, headers)?;
443
444        // Extract Prefer header
445        let prefer = headers
446            .and_then(|h| {
447                h.get("Prefer")
448                    .or_else(|| h.get("prefer"))
449                    .or_else(|| h.get("PREFER"))
450            })
451            .map(|p| parse_prefer_header(p))
452            .transpose()?;
453
454        // Parse RPC parameters (supports both GET and POST)
455        let params = parse_rpc_params(function_name, query_string, body)?;
456        return Ok(Operation::Rpc(params, prefer));
457    }
458
459    // Validate table name and schema (result used for validation)
460    let _resolved_table = resolve_schema(table, method, headers)?;
461
462    // Extract Prefer header (case-insensitive)
463    let prefer = headers
464        .and_then(|h| {
465            h.get("Prefer")
466                .or_else(|| h.get("prefer"))
467                .or_else(|| h.get("PREFER"))
468        })
469        .map(|p| parse_prefer_header(p))
470        .transpose()?;
471
472    match method.to_uppercase().as_str() {
473        "GET" => {
474            let params = parse_query_string(query_string)?;
475            Ok(Operation::Select(params, prefer))
476        }
477        "POST" => {
478            let body = body.ok_or_else(|| {
479                Error::Parse(ParseError::InvalidInsertBody(
480                    "Body is required for INSERT".to_string(),
481                ))
482            })?;
483            let params = parse_insert_params(query_string, body)?;
484            Ok(Operation::Insert(params, prefer))
485        }
486        "PUT" => {
487            // PUT is upsert: INSERT with automatic ON CONFLICT
488            let body = body.ok_or_else(|| {
489                Error::Parse(ParseError::InvalidInsertBody(
490                    "Body is required for PUT/upsert".to_string(),
491                ))
492            })?;
493            let mut params = parse_insert_params(query_string, body)?;
494
495            // If no ON CONFLICT specified, auto-create one from query filters
496            if params.on_conflict.is_none() {
497                // Extract column names from query string filters to use as conflict target
498                let conflict_columns = extract_conflict_columns_from_query(query_string);
499                if !conflict_columns.is_empty() {
500                    params = params.with_on_conflict(OnConflict::do_update(conflict_columns));
501                }
502            }
503
504            Ok(Operation::Insert(params, prefer))
505        }
506        "PATCH" => {
507            let body = body.ok_or_else(|| {
508                Error::Parse(ParseError::InvalidUpdateBody(
509                    "Body is required for UPDATE".to_string(),
510                ))
511            })?;
512            let params = parse_update_params(query_string, body)?;
513            Ok(Operation::Update(params, prefer))
514        }
515        "DELETE" => {
516            let params = parse_delete_params(query_string)?;
517            Ok(Operation::Delete(params, prefer))
518        }
519        _ => Err(Error::Parse(ParseError::UnsupportedMethod(format!(
520            "Unsupported HTTP method: {}",
521            method
522        )))),
523    }
524}
525
526/// Converts an Operation to SQL
527///
528/// # Arguments
529///
530/// * `table` - Table name (can be schema-qualified)
531/// * `operation` - The operation to convert
532///
533/// # Examples
534///
535/// ```
536/// use postgrest_parser::{parse, operation_to_sql};
537///
538/// let op = parse("GET", "users", "id=eq.123", None, None).unwrap();
539/// let result = operation_to_sql("users", &op).unwrap();
540/// assert!(result.query.contains("SELECT"));
541/// ```
542pub fn operation_to_sql(table: &str, operation: &Operation) -> Result<QueryResult, Error> {
543    #[cfg(any(feature = "postgres", feature = "wasm"))]
544    {
545        operation_to_sql_with_cache(table, operation, None)
546    }
547    #[cfg(not(any(feature = "postgres", feature = "wasm")))]
548    {
549        operation_to_sql_inner(table, operation, QueryBuilder::new)
550    }
551}
552
553/// Converts an Operation to SQL with an optional schema cache for relation resolution.
554///
555/// This variant allows passing a `SchemaCache` for resolving foreign key
556/// relationships in embedded resource queries (e.g., `select=*,posts(*)`)
557/// without requiring the `postgres` feature for database connectivity.
558#[cfg(any(feature = "postgres", feature = "wasm"))]
559pub fn operation_to_sql_with_cache(
560    table: &str,
561    operation: &Operation,
562    schema_cache: Option<std::sync::Arc<schema_cache::SchemaCache>>,
563) -> Result<QueryResult, Error> {
564    let make_builder = move || -> QueryBuilder {
565        let mut builder = QueryBuilder::new();
566        if let Some(cache) = &schema_cache {
567            builder = builder.with_schema_cache(cache.clone());
568        }
569        builder
570    };
571    operation_to_sql_inner(table, operation, make_builder)
572}
573
574fn operation_to_sql_inner(
575    table: &str,
576    operation: &Operation,
577    make_builder: impl Fn() -> QueryBuilder,
578) -> Result<QueryResult, Error> {
579    match operation {
580        Operation::Select(params, _prefer) => {
581            if table.is_empty() {
582                return Err(Error::Sql(SqlError::EmptyTableName));
583            }
584            let mut builder = make_builder();
585            builder.build_select(table, params).map_err(Error::Sql)
586        }
587        Operation::Insert(params, _prefer) => {
588            let resolved_table = resolve_schema(table, "POST", None)?;
589            let mut builder = make_builder();
590            builder
591                .build_insert(&resolved_table, params)
592                .map_err(Error::Sql)
593        }
594        Operation::Update(params, _prefer) => {
595            let resolved_table = resolve_schema(table, "PATCH", None)?;
596            let mut builder = make_builder();
597            builder
598                .build_update(&resolved_table, params)
599                .map_err(Error::Sql)
600        }
601        Operation::Delete(params, _prefer) => {
602            let resolved_table = resolve_schema(table, "DELETE", None)?;
603            let mut builder = make_builder();
604            builder
605                .build_delete(&resolved_table, params)
606                .map_err(Error::Sql)
607        }
608        Operation::Rpc(params, _prefer) => {
609            let function_name = table.strip_prefix("rpc/").unwrap_or(table);
610            let resolved_table = resolve_schema(function_name, "POST", None)?;
611            let mut builder = make_builder();
612            builder
613                .build_rpc(&resolved_table, params)
614                .map_err(Error::Sql)
615        }
616    }
617}
618
619/// Extracts column names from query string filters for use as ON CONFLICT target
620///
621/// Used by PUT requests to automatically determine conflict columns
622fn extract_conflict_columns_from_query(query_string: &str) -> Vec<String> {
623    if query_string.is_empty() {
624        return Vec::new();
625    }
626
627    let mut columns = Vec::new();
628    for pair in query_string.split('&') {
629        let parts: Vec<&str> = pair.splitn(2, '=').collect();
630        if parts.len() == 2 {
631            let key = parts[0];
632            // Skip reserved keys
633            if !parser::filter::reserved_key(key) && !parser::logic::logic_key(key) {
634                // Extract base column name (before any JSON operators)
635                let column_name = if let Some(arrow_pos) = key.find("->") {
636                    &key[..arrow_pos]
637                } else {
638                    key
639                };
640                if !columns.contains(&column_name.to_string()) {
641                    columns.push(column_name.to_string());
642                }
643            }
644        }
645    }
646    columns
647}
648
649fn parse_filters_from_map(
650    params: &std::collections::HashMap<String, String>,
651) -> Result<Vec<LogicCondition>, Error> {
652    let mut filters = Vec::new();
653
654    for (key, value) in params {
655        if parser::filter::reserved_key(key) {
656            continue;
657        }
658
659        if parser::logic::logic_key(key) {
660            let tree = parse_logic(key, value)?;
661            filters.push(LogicCondition::Logic(tree));
662        } else {
663            let filter = parse_filter(key, value)?;
664            filters.push(LogicCondition::Filter(filter));
665        }
666    }
667
668    Ok(filters)
669}
670
671/// Parses filters from a list of key-value pairs.
672///
673/// Unlike parse_filters_from_map, this function processes pairs sequentially,
674/// allowing multiple filters on the same column (e.g., price=gte.50&price=lte.150).
675fn parse_filters_from_pairs(pairs: &[(String, String)]) -> Result<Vec<LogicCondition>, Error> {
676    let mut filters = Vec::new();
677
678    for (key, value) in pairs {
679        if parser::filter::reserved_key(key) {
680            continue;
681        }
682
683        if parser::logic::logic_key(key) {
684            let tree = parse_logic(key, value)?;
685            filters.push(LogicCondition::Logic(tree));
686        } else {
687            let filter = parse_filter(key, value)?;
688            filters.push(LogicCondition::Filter(filter));
689        }
690    }
691
692    Ok(filters)
693}
694
695/// Result of building a filter clause.
696///
697/// Contains the SQL WHERE clause fragment and associated parameter values.
698#[derive(Debug, Clone, Serialize)]
699#[serde(rename_all = "camelCase")]
700pub struct FilterClauseResult {
701    /// The WHERE clause SQL fragment (without the "WHERE" keyword)
702    pub clause: String,
703    /// Parameter values referenced in the clause
704    pub params: Vec<serde_json::Value>,
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710
711    #[test]
712    fn test_parse_query_string_empty() {
713        let result = parse_query_string("");
714        assert!(result.is_ok());
715        let params = result.unwrap();
716        assert!(params.is_empty());
717    }
718
719    #[test]
720    fn test_parse_query_string_simple() {
721        let result = parse_query_string("select=id,name&id=eq.1");
722        assert!(result.is_ok());
723        let params = result.unwrap();
724        assert!(params.has_select());
725        assert!(params.has_filters());
726    }
727
728    #[test]
729    fn test_parse_query_string_with_order() {
730        let result = parse_query_string("select=id&order=id.desc");
731        assert!(result.is_ok());
732        let params = result.unwrap();
733        assert!(params.has_select());
734        assert!(!params.order.is_empty());
735    }
736
737    #[test]
738    fn test_parse_query_string_with_limit() {
739        let result = parse_query_string("select=id&limit=10");
740        assert!(result.is_ok());
741        let params = result.unwrap();
742        assert_eq!(params.limit, Some(10));
743    }
744
745    #[test]
746    fn test_to_sql_simple() {
747        let params = ParsedParams::new()
748            .with_select(vec![SelectItem::field("id"), SelectItem::field("name")]);
749
750        let result = to_sql("users", &params);
751        assert!(result.is_ok());
752        let query = result.unwrap();
753        assert!(query.query.contains("SELECT"));
754        assert!(query.query.contains("users"));
755    }
756
757    #[test]
758    fn test_query_string_to_sql() {
759        let result = query_string_to_sql("users", "select=id,name");
760        assert!(result.is_ok());
761        let query = result.unwrap();
762        assert!(query.query.contains("SELECT"));
763        assert!(query.query.contains("users"));
764        assert_eq!(query.tables, vec!["users"]);
765    }
766
767    #[test]
768    fn test_build_filter_clause() {
769        let filter = LogicCondition::Filter(Filter::new(
770            Field::new("id"),
771            FilterOperator::Eq,
772            FilterValue::Single("1".to_string()),
773        ));
774
775        let result = build_filter_clause(&[filter]);
776        assert!(result.is_ok());
777        let clause = result.unwrap();
778        assert!(clause.clause.contains("\"id\""));
779        assert!(clause.clause.contains("="));
780    }
781
782    #[test]
783    fn test_complex_query_with_multiple_filters() {
784        let query_str = "select=id,name,email&age=gte.18&status=in.(active,pending)&order=created_at.desc&limit=10";
785        let result = parse_query_string(query_str);
786        assert!(result.is_ok());
787        let params = result.unwrap();
788
789        assert!(params.has_select());
790        assert!(params.has_filters());
791        assert_eq!(params.filters.len(), 2);
792        assert_eq!(params.order.len(), 1);
793        assert_eq!(params.limit, Some(10));
794    }
795
796    #[test]
797    fn test_query_with_logic_operators() {
798        let query_str = "and=(age.gte.18,status.eq.active)";
799        let result = parse_query_string(query_str);
800        assert!(result.is_ok());
801        let params = result.unwrap();
802        assert!(params.has_filters());
803    }
804
805    #[test]
806    fn test_query_with_json_path() {
807        let query_str = "data->name=eq.John&data->age=gt.25";
808        let result = parse_query_string(query_str);
809        assert!(result.is_ok());
810        let params = result.unwrap();
811        assert_eq!(params.filters.len(), 2);
812    }
813
814    #[test]
815    fn test_query_with_type_cast() {
816        let query_str = "price::numeric=gt.100";
817        let result = parse_query_string(query_str);
818        assert!(result.is_ok());
819        let params = result.unwrap();
820        assert_eq!(params.filters.len(), 1);
821    }
822
823    #[test]
824    fn test_query_to_sql_with_comparison_operators() {
825        let query_str = "age=gte.18&price=lte.100";
826        let result = query_string_to_sql("users", query_str);
827        assert!(result.is_ok());
828        let query = result.unwrap();
829        assert!(query.query.contains(">="));
830        assert!(query.query.contains("<="));
831        assert_eq!(query.params.len(), 2);
832    }
833
834    #[test]
835    fn test_multiple_filters_same_column() {
836        // Test that multiple filters on the same column are ALL applied
837        let query_str = "price=gte.50&price=lte.150";
838        let params = parse_query_string(query_str).unwrap();
839
840        // Should have 2 filters, not 1 (bug was overwriting with HashMap)
841        assert_eq!(params.filters.len(), 2, "Should have both filters");
842
843        // Verify SQL generation includes both conditions
844        let result = query_string_to_sql("products", query_str).unwrap();
845        assert!(result.query.contains(">="), "Should have >= operator");
846        assert!(result.query.contains("<="), "Should have <= operator");
847        assert_eq!(result.params.len(), 2, "Should have 2 parameter values");
848
849        // Verify both conditions are in WHERE clause (AND logic)
850        assert!(result.query.contains("WHERE"));
851        assert!(result.query.contains("AND") || result.query.matches("price").count() == 2);
852    }
853
854    #[test]
855    fn test_query_to_sql_with_fts() {
856        let query_str = "content=fts(english).search term";
857        let result = query_string_to_sql("articles", query_str);
858        assert!(result.is_ok());
859        let query = result.unwrap();
860        assert!(query.query.contains("to_tsvector"));
861        assert!(query.query.contains("plainto_tsquery"));
862        assert!(query.query.contains("english"));
863    }
864
865    #[test]
866    fn test_query_to_sql_with_array_operators() {
867        let query_str = "tags=cs.{rust}";
868        let result = query_string_to_sql("posts", query_str);
869        assert!(result.is_ok());
870        let query = result.unwrap();
871        assert!(query.query.contains("@>"));
872    }
873
874    #[test]
875    fn test_query_to_sql_with_negation() {
876        let query_str = "status=not.eq.deleted";
877        let result = query_string_to_sql("users", query_str);
878        assert!(result.is_ok());
879        let query = result.unwrap();
880        assert!(query.query.contains("<>"));
881    }
882
883    #[test]
884    fn test_complex_nested_query() {
885        let query_str = "select=id,name,orders(id,total)&status=eq.active&age=gte.18&order=created_at.desc&limit=10&offset=20";
886        let result = parse_query_string(query_str);
887        assert!(result.is_ok());
888        let params = result.unwrap();
889
890        assert!(params.has_select());
891        assert_eq!(params.filters.len(), 2);
892        assert_eq!(params.order.len(), 1);
893        assert_eq!(params.limit, Some(10));
894        assert_eq!(params.offset, Some(20));
895    }
896
897    #[test]
898    fn test_query_with_quantifiers() {
899        let query_str = "tags=eq(any).{rust,elixir,go}";
900        let result = query_string_to_sql("posts", query_str);
901        assert!(result.is_ok());
902        let query = result.unwrap();
903        assert!(query.query.contains("= ANY"));
904    }
905
906    // Prefer header tests - Real-world scenarios
907
908    #[test]
909    fn test_insert_with_return_representation() {
910        // Real-world: User signup returning full user object
911        use std::collections::HashMap;
912        let mut headers = HashMap::new();
913        headers.insert("Prefer".to_string(), "return=representation".to_string());
914
915        let body = r#"{"email": "alice@example.com", "name": "Alice"}"#;
916        let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
917
918        match op {
919            Operation::Insert(_, Some(prefer)) => {
920                assert_eq!(
921                    prefer.return_representation,
922                    Some(ReturnRepresentation::Full)
923                );
924            }
925            _ => panic!("Expected Insert operation with Prefer"),
926        }
927    }
928
929    #[test]
930    fn test_insert_with_minimal_return() {
931        // Real-world: Bulk insert with minimal response
932        use std::collections::HashMap;
933        let mut headers = HashMap::new();
934        headers.insert("Prefer".to_string(), "return=minimal".to_string());
935
936        let body = r#"[{"name": "Alice"}, {"name": "Bob"}]"#;
937        let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
938
939        match op {
940            Operation::Insert(_, Some(prefer)) => {
941                assert_eq!(
942                    prefer.return_representation,
943                    Some(ReturnRepresentation::Minimal)
944                );
945            }
946            _ => panic!("Expected Insert with minimal return"),
947        }
948    }
949
950    #[test]
951    fn test_upsert_with_merge_duplicates() {
952        // Real-world: Upsert user preferences
953        use std::collections::HashMap;
954        let mut headers = HashMap::new();
955        headers.insert(
956            "Prefer".to_string(),
957            "resolution=merge-duplicates".to_string(),
958        );
959
960        let body = r#"{"user_id": 123, "theme": "dark"}"#;
961        let op = parse(
962            "POST",
963            "preferences",
964            "on_conflict=user_id",
965            Some(body),
966            Some(&headers),
967        )
968        .unwrap();
969
970        match op {
971            Operation::Insert(params, Some(prefer)) => {
972                assert_eq!(prefer.resolution, Some(Resolution::MergeDuplicates));
973                assert!(params.on_conflict.is_some());
974            }
975            _ => panic!("Expected Insert with resolution preference"),
976        }
977    }
978
979    #[test]
980    fn test_select_with_count_exact() {
981        // Real-world: Pagination with total count
982        use std::collections::HashMap;
983        let mut headers = HashMap::new();
984        headers.insert("Prefer".to_string(), "count=exact".to_string());
985
986        let op = parse("GET", "users", "limit=10&offset=0", None, Some(&headers)).unwrap();
987
988        match op {
989            Operation::Select(_, Some(prefer)) => {
990                assert_eq!(prefer.count, Some(Count::Exact));
991            }
992            _ => panic!("Expected Select with count"),
993        }
994    }
995
996    #[test]
997    fn test_multiple_prefer_options() {
998        // Real-world: Complex mutation with multiple preferences
999        use std::collections::HashMap;
1000        let mut headers = HashMap::new();
1001        headers.insert(
1002            "Prefer".to_string(),
1003            "return=representation, missing=default, plurality=singular".to_string(),
1004        );
1005
1006        let body = r#"{"name": "Bob"}"#;
1007        let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
1008
1009        match op {
1010            Operation::Insert(_, Some(prefer)) => {
1011                assert_eq!(
1012                    prefer.return_representation,
1013                    Some(ReturnRepresentation::Full)
1014                );
1015                assert_eq!(prefer.missing, Some(Missing::Default));
1016                assert_eq!(prefer.plurality, Some(Plurality::Singular));
1017            }
1018            _ => panic!("Expected Insert with multiple preferences"),
1019        }
1020    }
1021
1022    #[test]
1023    fn test_update_with_prefer_headers() {
1024        // Real-world: Update with return preference
1025        use std::collections::HashMap;
1026        let mut headers = HashMap::new();
1027        headers.insert("Prefer".to_string(), "return=representation".to_string());
1028
1029        let body = r#"{"status": "active"}"#;
1030        let op = parse("PATCH", "users", "id=eq.123", Some(body), Some(&headers)).unwrap();
1031
1032        match op {
1033            Operation::Update(_, Some(prefer)) => {
1034                assert_eq!(
1035                    prefer.return_representation,
1036                    Some(ReturnRepresentation::Full)
1037                );
1038            }
1039            _ => panic!("Expected Update with Prefer"),
1040        }
1041    }
1042
1043    #[test]
1044    fn test_delete_with_prefer_headers() {
1045        // Real-world: Delete with headers-only return
1046        use std::collections::HashMap;
1047        let mut headers = HashMap::new();
1048        headers.insert("Prefer".to_string(), "return=headers-only".to_string());
1049
1050        let op = parse("DELETE", "users", "status=eq.deleted", None, Some(&headers)).unwrap();
1051
1052        match op {
1053            Operation::Delete(_, Some(prefer)) => {
1054                assert_eq!(
1055                    prefer.return_representation,
1056                    Some(ReturnRepresentation::HeadersOnly)
1057                );
1058            }
1059            _ => panic!("Expected Delete with Prefer"),
1060        }
1061    }
1062
1063    #[test]
1064    fn test_prefer_header_case_insensitive() {
1065        // Test that Prefer header is case-insensitive
1066        use std::collections::HashMap;
1067        let mut headers = HashMap::new();
1068        headers.insert("prefer".to_string(), "count=exact".to_string());
1069
1070        let op = parse("GET", "users", "", None, Some(&headers)).unwrap();
1071
1072        match op {
1073            Operation::Select(_, Some(prefer)) => {
1074                assert_eq!(prefer.count, Some(Count::Exact));
1075            }
1076            _ => panic!("Expected Select with Prefer"),
1077        }
1078    }
1079
1080    #[test]
1081    fn test_no_prefer_headers() {
1082        // Test operation without Prefer headers
1083        let op = parse("GET", "users", "id=eq.123", None, None).unwrap();
1084
1085        match op {
1086            Operation::Select(_, prefer) => {
1087                assert!(prefer.is_none());
1088            }
1089            _ => panic!("Expected Select without Prefer"),
1090        }
1091    }
1092
1093    #[test]
1094    fn test_prefer_with_schema_headers() {
1095        // Real-world: Combine Prefer with Content-Profile
1096        use std::collections::HashMap;
1097        let mut headers = HashMap::new();
1098        headers.insert("Prefer".to_string(), "return=representation".to_string());
1099        headers.insert("Content-Profile".to_string(), "auth".to_string());
1100
1101        let body = r#"{"email": "alice@example.com"}"#;
1102        let op = parse("POST", "users", "", Some(body), Some(&headers)).unwrap();
1103
1104        match op {
1105            Operation::Insert(_, Some(prefer)) => {
1106                assert_eq!(
1107                    prefer.return_representation,
1108                    Some(ReturnRepresentation::Full)
1109                );
1110            }
1111            _ => panic!("Expected Insert with both Prefer and schema headers"),
1112        }
1113    }
1114
1115    // RPC Integration Tests
1116
1117    #[test]
1118    fn test_rpc_post_with_args() {
1119        // Real-world: Call stored procedure with arguments
1120        let body = r#"{"user_id": 123, "status": "active"}"#;
1121        let op = parse("POST", "rpc/get_user_posts", "", Some(body), None).unwrap();
1122
1123        match op {
1124            Operation::Rpc(params, prefer) => {
1125                assert_eq!(params.function_name, "get_user_posts");
1126                assert_eq!(params.args.len(), 2);
1127                assert!(prefer.is_none());
1128            }
1129            _ => panic!("Expected RPC operation"),
1130        }
1131    }
1132
1133    #[test]
1134    fn test_rpc_get_no_args() {
1135        // Real-world: Health check or utility function
1136        let op = parse("GET", "rpc/health_check", "", None, None).unwrap();
1137
1138        match op {
1139            Operation::Rpc(params, _) => {
1140                assert_eq!(params.function_name, "health_check");
1141                assert!(params.args.is_empty());
1142            }
1143            _ => panic!("Expected RPC operation"),
1144        }
1145    }
1146
1147    #[test]
1148    fn test_rpc_with_filters() {
1149        // Real-world: Call function and filter results
1150        let body = r#"{"department_id": 5}"#;
1151        let query = "age=gte.25&salary=lt.100000";
1152        let op = parse("POST", "rpc/find_employees", query, Some(body), None).unwrap();
1153
1154        match op {
1155            Operation::Rpc(params, _) => {
1156                assert_eq!(params.function_name, "find_employees");
1157                assert_eq!(params.filters.len(), 2);
1158            }
1159            _ => panic!("Expected RPC operation"),
1160        }
1161    }
1162
1163    #[test]
1164    fn test_rpc_with_order_limit() {
1165        // Real-world: Paginated function results
1166        let query = "order=created_at.desc&limit=10&offset=20";
1167        let op = parse("GET", "rpc/list_recent_posts", query, None, None).unwrap();
1168
1169        match op {
1170            Operation::Rpc(params, _) => {
1171                assert_eq!(params.function_name, "list_recent_posts");
1172                assert_eq!(params.order.len(), 1);
1173                assert_eq!(params.limit, Some(10));
1174                assert_eq!(params.offset, Some(20));
1175            }
1176            _ => panic!("Expected RPC operation"),
1177        }
1178    }
1179
1180    #[test]
1181    fn test_rpc_with_select() {
1182        // Real-world: Select specific columns from function results
1183        let body = r#"{"search_term": "laptop"}"#;
1184        let query = "select=id,name,price";
1185        let op = parse("POST", "rpc/search_products", query, Some(body), None).unwrap();
1186
1187        match op {
1188            Operation::Rpc(params, _) => {
1189                assert_eq!(params.function_name, "search_products");
1190                assert!(params.returning.is_some());
1191                assert_eq!(params.returning.unwrap().len(), 3);
1192            }
1193            _ => panic!("Expected RPC operation"),
1194        }
1195    }
1196
1197    #[test]
1198    fn test_rpc_with_prefer_headers() {
1199        // Real-world: RPC with Prefer header for response preferences
1200        use std::collections::HashMap;
1201        let mut headers = HashMap::new();
1202        headers.insert("Prefer".to_string(), "return=representation".to_string());
1203
1204        let body = r#"{"amount": 100.50}"#;
1205        let op = parse(
1206            "POST",
1207            "rpc/process_payment",
1208            "",
1209            Some(body),
1210            Some(&headers),
1211        )
1212        .unwrap();
1213
1214        match op {
1215            Operation::Rpc(params, Some(prefer)) => {
1216                assert_eq!(params.function_name, "process_payment");
1217                assert_eq!(
1218                    prefer.return_representation,
1219                    Some(ReturnRepresentation::Full)
1220                );
1221            }
1222            _ => panic!("Expected RPC operation with Prefer header"),
1223        }
1224    }
1225
1226    #[test]
1227    fn test_rpc_to_sql_simple() {
1228        // Real-world: Generate SQL for simple function call
1229        let body = r#"{"user_id": 42}"#;
1230        let op = parse("POST", "rpc/get_profile", "", Some(body), None).unwrap();
1231        let result = operation_to_sql("rpc/get_profile", &op).unwrap();
1232
1233        assert!(result.query.contains(r#"FROM "public"."get_profile"("#));
1234        assert!(result.query.contains(r#""user_id" := $1"#));
1235        assert_eq!(result.params.len(), 1);
1236    }
1237
1238    #[test]
1239    fn test_rpc_to_sql_with_schema() {
1240        // Real-world: Function in custom schema using qualified name
1241        let body = r#"{"query": "test"}"#;
1242        let op = parse("POST", "rpc/api.search", "", Some(body), None).unwrap();
1243        let result = operation_to_sql("rpc/api.search", &op).unwrap();
1244
1245        assert!(result.query.contains(r#"FROM "api"."search"("#));
1246    }
1247
1248    #[test]
1249    fn test_rpc_to_sql_complex() {
1250        // Real-world: Complex function call with all features
1251        let body = r#"{"min_price": 100, "max_price": 1000}"#;
1252        let query = "category=eq.electronics&in_stock=eq.true&order=price.asc&limit=20&select=id,name,price";
1253        let op = parse("POST", "rpc/find_products", query, Some(body), None).unwrap();
1254        let result = operation_to_sql("rpc/find_products", &op).unwrap();
1255
1256        assert!(result.query.contains(r#"FROM "public"."find_products"("#));
1257        assert!(result.query.contains(r#""max_price" := $1"#));
1258        assert!(result.query.contains(r#""min_price" := $2"#));
1259        assert!(result.query.contains("WHERE"));
1260        assert!(result.query.contains("ORDER BY"));
1261        assert!(result.query.contains("LIMIT"));
1262        assert!(result.params.len() > 2);
1263    }
1264
1265    #[test]
1266    fn test_rpc_invalid_empty_function_name() {
1267        // Edge case: Empty function name
1268        let result = parse("POST", "rpc/", "", None, None);
1269        assert!(result.is_err());
1270    }
1271
1272    #[test]
1273    fn test_rpc_get_with_query_params() {
1274        // Real-world: GET request with query parameters (args in query string)
1275        // Note: This is less common but supported
1276        let query = "limit=5";
1277        let op = parse("GET", "rpc/get_stats", query, None, None).unwrap();
1278
1279        match op {
1280            Operation::Rpc(params, _) => {
1281                assert_eq!(params.function_name, "get_stats");
1282                assert_eq!(params.limit, Some(5));
1283            }
1284            _ => panic!("Expected RPC operation"),
1285        }
1286    }
1287
1288    // Phase 5: Resource Embedding Tests
1289
1290    #[test]
1291    fn test_insert_with_select_parameter() {
1292        // Real-world: Insert and return specific columns using 'select' parameter
1293        let body = r#"{"email": "bob@example.com", "name": "Bob"}"#;
1294        let query = "select=id,email,created_at";
1295        let op = parse("POST", "users", query, Some(body), None).unwrap();
1296
1297        match op {
1298            Operation::Insert(params, _) => {
1299                assert!(params.returning.is_some());
1300                let returning = params.returning.unwrap();
1301                assert_eq!(returning.len(), 3);
1302                assert_eq!(returning[0].name, "id");
1303                assert_eq!(returning[1].name, "email");
1304                assert_eq!(returning[2].name, "created_at");
1305            }
1306            _ => panic!("Expected Insert with select"),
1307        }
1308    }
1309
1310    #[test]
1311    fn test_update_with_select_parameter() {
1312        // Real-world: Update and return specific columns
1313        let body = r#"{"status": "verified"}"#;
1314        let query = "id=eq.123&select=id,status,updated_at";
1315        let op = parse("PATCH", "users", query, Some(body), None).unwrap();
1316
1317        match op {
1318            Operation::Update(params, _) => {
1319                assert!(params.returning.is_some());
1320                let returning = params.returning.unwrap();
1321                assert_eq!(returning.len(), 3);
1322            }
1323            _ => panic!("Expected Update with select"),
1324        }
1325    }
1326
1327    #[test]
1328    fn test_delete_with_select_parameter() {
1329        // Real-world: Delete and return deleted rows
1330        let query = "status=eq.inactive&select=id,email";
1331        let op = parse("DELETE", "users", query, None, None).unwrap();
1332
1333        match op {
1334            Operation::Delete(params, _) => {
1335                assert!(params.returning.is_some());
1336                let returning = params.returning.unwrap();
1337                assert_eq!(returning.len(), 2);
1338            }
1339            _ => panic!("Expected Delete with select"),
1340        }
1341    }
1342
1343    #[test]
1344    fn test_insert_with_returning_backwards_compat() {
1345        // Backwards compatibility: 'returning' parameter still works
1346        let body = r#"{"email": "alice@example.com"}"#;
1347        let query = "returning=id,created_at";
1348        let op = parse("POST", "users", query, Some(body), None).unwrap();
1349
1350        match op {
1351            Operation::Insert(params, _) => {
1352                assert!(params.returning.is_some());
1353                assert_eq!(params.returning.unwrap().len(), 2);
1354            }
1355            _ => panic!("Expected Insert with returning"),
1356        }
1357    }
1358
1359    #[test]
1360    fn test_select_takes_precedence_over_returning() {
1361        // Real-world: If both 'select' and 'returning' are provided, 'select' wins
1362        let body = r#"{"email": "test@example.com"}"#;
1363        let query = "select=id&returning=id,email,name";
1364        let op = parse("POST", "users", query, Some(body), None).unwrap();
1365
1366        match op {
1367            Operation::Insert(params, _) => {
1368                assert!(params.returning.is_some());
1369                let returning = params.returning.unwrap();
1370                // Should use 'select' parameter, which has only 'id'
1371                assert_eq!(returning.len(), 1);
1372                assert_eq!(returning[0].name, "id");
1373            }
1374            _ => panic!("Expected Insert"),
1375        }
1376    }
1377
1378    #[test]
1379    fn test_mutation_select_to_sql() {
1380        // Real-world: Verify SQL generation with select parameter
1381        let body = r#"{"name": "New Product", "price": 99.99}"#;
1382        let query = "select=id,name,created_at";
1383        let op = parse("POST", "products", query, Some(body), None).unwrap();
1384        let result = operation_to_sql("products", &op).unwrap();
1385
1386        assert!(result.query.contains("RETURNING"));
1387        assert!(result.query.contains(r#""id""#));
1388        assert!(result.query.contains(r#""name""#));
1389        assert!(result.query.contains(r#""created_at""#));
1390    }
1391
1392    // Phase 6: PUT Upsert Tests
1393
1394    #[test]
1395    fn test_put_upsert_basic() {
1396        // Real-world: PUT upserts based on query filter columns
1397        let body = r#"{"email": "alice@example.com", "name": "Alice Updated"}"#;
1398        let query = "email=eq.alice@example.com";
1399        let op = parse("PUT", "users", query, Some(body), None).unwrap();
1400
1401        match op {
1402            Operation::Insert(params, _) => {
1403                assert!(params.on_conflict.is_some());
1404                let conflict = params.on_conflict.unwrap();
1405                assert_eq!(conflict.columns, vec!["email"]);
1406                assert_eq!(conflict.action, ConflictAction::DoUpdate);
1407            }
1408            _ => panic!("Expected Insert (upsert) operation"),
1409        }
1410    }
1411
1412    #[test]
1413    fn test_put_upsert_multiple_columns() {
1414        // Real-world: Upsert with multiple conflict columns
1415        let body = r#"{"email": "bob@example.com", "team": "engineering", "role": "senior"}"#;
1416        let query = "email=eq.bob@example.com&team=eq.engineering";
1417        let op = parse("PUT", "users", query, Some(body), None).unwrap();
1418
1419        match op {
1420            Operation::Insert(params, _) => {
1421                assert!(params.on_conflict.is_some());
1422                let conflict = params.on_conflict.unwrap();
1423                assert_eq!(conflict.columns.len(), 2);
1424                assert!(conflict.columns.contains(&"email".to_string()));
1425                assert!(conflict.columns.contains(&"team".to_string()));
1426            }
1427            _ => panic!("Expected Insert with multi-column conflict"),
1428        }
1429    }
1430
1431    #[test]
1432    fn test_put_with_explicit_on_conflict() {
1433        // Real-world: PUT with explicit ON CONFLICT overrides auto-detection
1434        let body = r#"{"id": 123, "name": "Test"}"#;
1435        let query = "id=eq.123&on_conflict=id";
1436        let op = parse("PUT", "items", query, Some(body), None).unwrap();
1437
1438        match op {
1439            Operation::Insert(params, _) => {
1440                assert!(params.on_conflict.is_some());
1441                // Explicit on_conflict from query string should be used
1442                let conflict = params.on_conflict.unwrap();
1443                assert_eq!(conflict.columns, vec!["id"]);
1444            }
1445            _ => panic!("Expected Insert"),
1446        }
1447    }
1448
1449    #[test]
1450    fn test_put_without_filters() {
1451        // Edge case: PUT without filters doesn't add ON CONFLICT
1452        let body = r#"{"name": "New Item"}"#;
1453        let op = parse("PUT", "items", "", Some(body), None).unwrap();
1454
1455        match op {
1456            Operation::Insert(params, _) => {
1457                // No conflict columns from filters, so no ON CONFLICT
1458                assert!(params.on_conflict.is_none());
1459            }
1460            _ => panic!("Expected Insert"),
1461        }
1462    }
1463
1464    #[test]
1465    fn test_put_to_sql() {
1466        // Real-world: Verify PUT generates proper upsert SQL
1467        let body = r#"{"email": "test@example.com", "name": "Test User"}"#;
1468        let query = "email=eq.test@example.com&select=id,email,name";
1469        let op = parse("PUT", "users", query, Some(body), None).unwrap();
1470        let result = operation_to_sql("users", &op).unwrap();
1471
1472        assert!(result.query.contains("INSERT INTO"));
1473        assert!(result.query.contains("ON CONFLICT"));
1474        assert!(result.query.contains("DO UPDATE SET"));
1475        assert!(result.query.contains("RETURNING"));
1476    }
1477
1478    #[test]
1479    fn test_put_requires_body() {
1480        // Error case: PUT without body should fail
1481        let result = parse("PUT", "users", "id=eq.123", None, None);
1482        assert!(result.is_err());
1483    }
1484
1485    // Phase 7: Advanced ON CONFLICT Tests
1486
1487    #[test]
1488    fn test_on_conflict_with_where_clause() {
1489        // Real-world: Partial unique index with WHERE clause
1490        use crate::parser::parse_filter;
1491
1492        let body = r#"{"email": "alice@example.com", "name": "Alice"}"#;
1493        let mut params = parse_insert_params("", body).unwrap();
1494
1495        // Manually create advanced ON CONFLICT (parser extension would go here)
1496        let filter = parse_filter("deleted_at", "is.null").unwrap();
1497        let conflict = OnConflict::do_update(vec!["email".to_string()])
1498            .with_where_clause(vec![LogicCondition::Filter(filter)]);
1499
1500        params = params.with_on_conflict(conflict);
1501        let op = Operation::Insert(params, None);
1502        let result = operation_to_sql("users", &op).unwrap();
1503
1504        assert!(result.query.contains("ON CONFLICT"));
1505        assert!(result.query.contains(r#"("email")"#));
1506        assert!(result.query.contains("WHERE"));
1507        assert!(result.query.contains("deleted_at"));
1508    }
1509
1510    #[test]
1511    fn test_on_conflict_with_specific_update_columns() {
1512        // Real-world: Only update specific columns on conflict
1513        let body = r#"{"email": "bob@example.com", "name": "Bob", "role": "admin"}"#;
1514        let mut params = parse_insert_params("", body).unwrap();
1515
1516        // Create ON CONFLICT that only updates 'name' column, not 'role'
1517        let conflict = OnConflict::do_update(vec!["email".to_string()])
1518            .with_update_columns(vec!["name".to_string()]);
1519
1520        params = params.with_on_conflict(conflict);
1521        let op = Operation::Insert(params, None);
1522        let result = operation_to_sql("users", &op).unwrap();
1523
1524        assert!(result.query.contains("ON CONFLICT"));
1525        assert!(result.query.contains(r#""name" = EXCLUDED."name""#));
1526        // Role should NOT be in the update
1527        assert!(!result.query.contains(r#""role" = EXCLUDED."role""#));
1528    }
1529
1530    #[test]
1531    fn test_on_conflict_complex() {
1532        // Real-world: Partial unique index with specific update columns
1533        use crate::parser::parse_filter;
1534
1535        let body = r#"{"user_id": 123, "post_id": 456, "reaction": "like"}"#;
1536        let mut params = parse_insert_params("", body).unwrap();
1537
1538        let filter = parse_filter("deleted_at", "is.null").unwrap();
1539        let conflict = OnConflict::do_update(vec!["user_id".to_string(), "post_id".to_string()])
1540            .with_where_clause(vec![LogicCondition::Filter(filter)])
1541            .with_update_columns(vec!["reaction".to_string()]);
1542
1543        params = params.with_on_conflict(conflict);
1544        let op = Operation::Insert(params, None);
1545        let result = operation_to_sql("reactions", &op).unwrap();
1546
1547        // Columns might be in either order
1548        println!("SQL: {}", result.query);
1549        assert!(
1550            result
1551                .query
1552                .contains(r#"ON CONFLICT ("post_id", "user_id")"#)
1553                || result
1554                    .query
1555                    .contains(r#"ON CONFLICT ("user_id", "post_id")"#)
1556        );
1557        assert!(result.query.contains("WHERE"));
1558        assert!(result.query.contains(r#""reaction" = EXCLUDED."reaction""#));
1559    }
1560
1561    // Real-World Scenario Tests (100% Parity Demonstration)
1562
1563    #[test]
1564    fn test_ecommerce_workflow() {
1565        use std::collections::HashMap;
1566
1567        // 1. Bulk insert order items with return representation
1568        let body = r#"[
1569            {"product_id": 1, "quantity": 2, "price": 29.99},
1570            {"product_id": 3, "quantity": 1, "price": 49.99}
1571        ]"#;
1572        let mut headers = HashMap::new();
1573        headers.insert("Prefer".to_string(), "return=representation".to_string());
1574        headers.insert("Content-Profile".to_string(), "sales".to_string());
1575
1576        let op = parse(
1577            "POST",
1578            "order_items",
1579            "select=*",
1580            Some(body),
1581            Some(&headers),
1582        )
1583        .unwrap();
1584        match op {
1585            Operation::Insert(params, Some(prefer)) => {
1586                assert_eq!(
1587                    prefer.return_representation,
1588                    Some(ReturnRepresentation::Full)
1589                );
1590                assert!(params.returning.is_some());
1591            }
1592            _ => panic!("Expected Insert with Prefer"),
1593        }
1594
1595        // 2. Update order status with specific columns returned
1596        let body = r#"{"status": "shipped", "shipped_at": "2024-01-15"}"#;
1597        let op = parse(
1598            "PATCH",
1599            "orders",
1600            "id=eq.123&select=id,status,shipped_at",
1601            Some(body),
1602            None,
1603        )
1604        .unwrap();
1605        match op {
1606            Operation::Update(params, _) => {
1607                assert!(params.has_filters());
1608                assert!(params.returning.is_some());
1609            }
1610            _ => panic!("Expected Update"),
1611        }
1612
1613        // 3. Calculate total with RPC
1614        let body = r#"{"order_id": 123}"#;
1615        let op = parse("POST", "rpc/calculate_order_total", "", Some(body), None).unwrap();
1616        match op {
1617            Operation::Rpc(params, _) => {
1618                assert_eq!(params.function_name, "calculate_order_total");
1619            }
1620            _ => panic!("Expected RPC"),
1621        }
1622    }
1623
1624    #[test]
1625    fn test_social_media_workflow() {
1626        use std::collections::HashMap;
1627
1628        // 1. Create post with embedded user data
1629        let body = r#"{"content": "Hello World!", "user_id": 456}"#;
1630        let mut headers = HashMap::new();
1631        headers.insert("Prefer".to_string(), "return=representation".to_string());
1632
1633        let op = parse(
1634            "POST",
1635            "posts",
1636            "select=id,content,user_id",
1637            Some(body),
1638            Some(&headers),
1639        )
1640        .unwrap();
1641        match op {
1642            Operation::Insert(_, Some(prefer)) => {
1643                assert_eq!(
1644                    prefer.return_representation,
1645                    Some(ReturnRepresentation::Full)
1646                );
1647            }
1648            _ => panic!("Expected Insert"),
1649        }
1650
1651        // 2. Upsert like with PUT
1652        let body = r#"{"user_id": 789, "post_id": 123}"#;
1653        let op = parse(
1654            "PUT",
1655            "likes",
1656            "user_id=eq.789&post_id=eq.123",
1657            Some(body),
1658            None,
1659        )
1660        .unwrap();
1661        match op {
1662            Operation::Insert(params, _) => {
1663                assert!(params.on_conflict.is_some());
1664            }
1665            _ => panic!("Expected upsert"),
1666        }
1667
1668        // 3. Delete old posts with limit
1669        let op = parse(
1670            "DELETE",
1671            "posts",
1672            "created_at=lt.2020-01-01&order=created_at.asc&limit=100",
1673            None,
1674            None,
1675        )
1676        .unwrap();
1677        match op {
1678            Operation::Delete(params, _) => {
1679                assert!(params.has_filters());
1680                assert_eq!(params.limit, Some(100));
1681            }
1682            _ => panic!("Expected Delete"),
1683        }
1684    }
1685
1686    #[test]
1687    fn test_analytics_workflow() {
1688        use std::collections::HashMap;
1689
1690        // 1. Bulk upsert metrics with merge duplicates
1691        let body = r#"[
1692            {"metric": "pageviews", "value": 1234, "date": "2024-01-15"},
1693            {"metric": "signups", "value": 56, "date": "2024-01-15"}
1694        ]"#;
1695        let mut headers = HashMap::new();
1696        headers.insert(
1697            "Prefer".to_string(),
1698            "resolution=merge-duplicates".to_string(),
1699        );
1700
1701        let op = parse(
1702            "POST",
1703            "metrics",
1704            "on_conflict=metric,date",
1705            Some(body),
1706            Some(&headers),
1707        )
1708        .unwrap();
1709        match op {
1710            Operation::Insert(params, Some(prefer)) => {
1711                assert!(params.on_conflict.is_some());
1712                assert_eq!(prefer.resolution, Some(Resolution::MergeDuplicates));
1713            }
1714            _ => panic!("Expected Insert with resolution"),
1715        }
1716
1717        // 2. Get aggregated stats with RPC and filtering
1718        let body = r#"{"start_date": "2024-01-01", "end_date": "2024-01-31"}"#;
1719        let op = parse(
1720            "POST",
1721            "rpc/get_monthly_stats",
1722            "metric=eq.pageviews",
1723            Some(body),
1724            None,
1725        )
1726        .unwrap();
1727        match op {
1728            Operation::Rpc(params, _) => {
1729                assert_eq!(params.function_name, "get_monthly_stats");
1730                assert!(!params.filters.is_empty());
1731            }
1732            _ => panic!("Expected RPC with filters"),
1733        }
1734
1735        // 3. Count with prefer header
1736        let mut headers = HashMap::new();
1737        headers.insert("Prefer".to_string(), "count=exact".to_string());
1738
1739        let op = parse(
1740            "GET",
1741            "events",
1742            "created_at=gte.2024-01-01",
1743            None,
1744            Some(&headers),
1745        )
1746        .unwrap();
1747        match op {
1748            Operation::Select(_, Some(prefer)) => {
1749                assert_eq!(prefer.count, Some(Count::Exact));
1750            }
1751            _ => panic!("Expected Select with count"),
1752        }
1753    }
1754
1755    // Resource Embedding Tests (PostgREST select with relations)
1756
1757    #[test]
1758    fn test_embedding_many_to_one_via_fk() {
1759        let result = query_string_to_sql("posts", "select=*,profiles(username,avatar_url)");
1760        assert!(result.is_ok());
1761        let query = result.unwrap();
1762        assert!(query.query.contains("SELECT"));
1763        assert!(query.query.contains("profiles"));
1764        // row_to_json takes a single record, not individual columns
1765        assert!(
1766            !query.query.contains("row_to_json(profiles.\"username\""),
1767            "row_to_json must not receive individual columns: {}",
1768            query.query
1769        );
1770        assert!(
1771            query.query.contains("row_to_json("),
1772            "should use row_to_json with a subquery record: {}",
1773            query.query
1774        );
1775    }
1776
1777    #[test]
1778    fn test_embedding_one_to_many() {
1779        let result = query_string_to_sql("posts", "select=title,comments(id,body)");
1780        assert!(result.is_ok());
1781        let query = result.unwrap();
1782        assert!(query.query.contains("\"title\""));
1783        assert!(query.query.contains("comments"));
1784        // row_to_json takes a single record, not individual columns
1785        assert!(
1786            !query.query.contains("row_to_json(comments.\"id\""),
1787            "row_to_json must not receive individual columns: {}",
1788            query.query
1789        );
1790    }
1791
1792    #[test]
1793    fn test_embedding_select_star_produces_valid_row_to_json() {
1794        let result = query_string_to_sql("posts", "select=*,comments(*)");
1795        assert!(result.is_ok());
1796        let query = result.unwrap();
1797        assert!(
1798            query.query.contains("row_to_json("),
1799            "should use row_to_json: {}",
1800            query.query
1801        );
1802    }
1803
1804    #[test]
1805    fn test_embedding_nested_produces_valid_sql() {
1806        let result = query_string_to_sql(
1807            "posts",
1808            "select=id,comments(id,body,author:profiles(name,avatar_url))",
1809        );
1810        assert!(result.is_ok());
1811        let query = result.unwrap();
1812        assert!(
1813            !query.query.contains("row_to_json(profiles.\"name\""),
1814            "nested row_to_json must not receive individual columns: {}",
1815            query.query
1816        );
1817    }
1818
1819    #[test]
1820    fn test_embedding_aliased_relation() {
1821        // select("*, author:profiles(name)") — aliased relation
1822        let params = parse_query_string("select=*,author:profiles(name)").unwrap();
1823        let select = params.select.as_ref().unwrap();
1824        let relation = &select[1];
1825        assert_eq!(relation.name, "profiles");
1826        assert_eq!(relation.alias, Some("author".to_string()));
1827        assert_eq!(relation.item_type, ItemType::Relation);
1828    }
1829
1830    #[test]
1831    fn test_embedding_nested_with_alias() {
1832        // select("*, comments(id, author:profiles(name))") — nested embedding with alias
1833        let params = parse_query_string("select=*,comments(id,author:profiles(name))").unwrap();
1834        let select = params.select.as_ref().unwrap();
1835        let comments = &select[1];
1836        assert_eq!(comments.name, "comments");
1837        let children = comments.children.as_ref().unwrap();
1838        assert_eq!(children[1].name, "profiles");
1839        assert_eq!(children[1].alias, Some("author".to_string()));
1840        assert_eq!(children[1].item_type, ItemType::Relation);
1841        let nested = children[1].children.as_ref().unwrap();
1842        assert_eq!(nested[0].name, "name");
1843    }
1844
1845    #[test]
1846    fn test_embedding_fk_hint_disambiguation() {
1847        // select("*, author:profiles!author_id_fkey(name)") — FK hint
1848        let params = parse_query_string("select=*,author:profiles!author_id_fkey(name)").unwrap();
1849        let select = params.select.as_ref().unwrap();
1850        let relation = &select[1];
1851        assert_eq!(relation.name, "profiles");
1852        assert_eq!(relation.alias, Some("author".to_string()));
1853        assert!(relation.hint.is_some());
1854        assert_eq!(
1855            relation.hint,
1856            Some(ItemHint::Inner("author_id_fkey".to_string()))
1857        );
1858    }
1859
1860    #[test]
1861    fn test_embedding_with_filters_and_ordering() {
1862        // Real-world: Select with embedding + filters + ordering
1863        let query_str = "select=id,title,author:profiles(name,avatar_url),comments(id,body)&status=eq.published&order=created_at.desc&limit=10";
1864        let params = parse_query_string(query_str).unwrap();
1865
1866        assert!(params.has_select());
1867        let select = params.select.as_ref().unwrap();
1868        assert_eq!(select.len(), 4); // id, title, profiles (aliased as author), comments
1869        assert_eq!(select[2].alias, Some("author".to_string()));
1870        assert_eq!(select[3].name, "comments");
1871
1872        assert!(params.has_filters());
1873        assert_eq!(params.order.len(), 1);
1874        assert_eq!(params.limit, Some(10));
1875    }
1876
1877    #[test]
1878    fn test_embedding_supabase_blog_example() {
1879        // Real Supabase use case: Blog post with author and comments
1880        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";
1881        let params = parse_query_string(query_str).unwrap();
1882
1883        let select = params.select.as_ref().unwrap();
1884        assert_eq!(select.len(), 5); // id, title, content, author:profiles, comments
1885
1886        // Author relation with FK hint
1887        let author = &select[3];
1888        assert_eq!(author.name, "profiles");
1889        assert_eq!(author.alias, Some("author".to_string()));
1890        assert_eq!(
1891            author.hint,
1892            Some(ItemHint::Inner("author_id_fkey".to_string()))
1893        );
1894
1895        // Comments with nested commenter relation
1896        let comments = &select[4];
1897        assert_eq!(comments.name, "comments");
1898        let comment_children = comments.children.as_ref().unwrap();
1899        assert_eq!(comment_children.len(), 4); // id, body, created_at, commenter:profiles
1900
1901        let commenter = &comment_children[3];
1902        assert_eq!(commenter.name, "profiles");
1903        assert_eq!(commenter.alias, Some("commenter".to_string()));
1904        assert_eq!(
1905            commenter.hint,
1906            Some(ItemHint::Inner("commenter_id_fkey".to_string()))
1907        );
1908    }
1909
1910    #[test]
1911    fn test_100_percent_parity_demonstration() {
1912        // Comprehensive test demonstrating all PostgREST features
1913        use std::collections::HashMap;
1914
1915        // Feature 1: Full mutation support (INSERT, UPDATE, DELETE, PUT)
1916        let body = r#"{"email": "test@example.com"}"#;
1917        assert!(parse("POST", "users", "", Some(body), None).is_ok());
1918        assert!(parse("PUT", "users", "id=eq.1", Some(body), None).is_ok());
1919        assert!(parse("PATCH", "users", "id=eq.1", Some(body), None).is_ok());
1920        assert!(parse("DELETE", "users", "id=eq.1", None, None).is_ok());
1921
1922        // Feature 2: RPC function calls
1923        assert!(parse("POST", "rpc/my_function", "", Some(body), None).is_ok());
1924        assert!(parse("GET", "rpc/my_function", "", None, None).is_ok());
1925
1926        // Feature 3: Prefer headers (all 5 types)
1927        let mut headers = HashMap::new();
1928        headers.insert(
1929            "Prefer".to_string(),
1930            "return=representation, count=exact, resolution=merge-duplicates, plurality=singular, missing=default".to_string(),
1931        );
1932        let op = parse("GET", "users", "", None, Some(&headers)).unwrap();
1933        match op {
1934            Operation::Select(_, Some(prefer)) => {
1935                assert_eq!(
1936                    prefer.return_representation,
1937                    Some(ReturnRepresentation::Full)
1938                );
1939                assert_eq!(prefer.count, Some(Count::Exact));
1940                assert_eq!(prefer.resolution, Some(Resolution::MergeDuplicates));
1941                assert_eq!(prefer.plurality, Some(Plurality::Singular));
1942                assert_eq!(prefer.missing, Some(Missing::Default));
1943            }
1944            _ => panic!("Expected all prefer options"),
1945        }
1946
1947        // Feature 4: Schema qualification via headers
1948        let mut headers = HashMap::new();
1949        headers.insert("Accept-Profile".to_string(), "api".to_string());
1950        assert!(parse("GET", "users", "", None, Some(&headers)).is_ok());
1951
1952        // Feature 5: Advanced filtering, ordering, pagination
1953        assert!(parse(
1954            "GET",
1955            "users",
1956            "age=gte.18&status=in.(active,verified)&order=created_at.desc&limit=10&offset=20&select=id,name",
1957            None,
1958            None
1959        )
1960        .is_ok());
1961
1962        // Feature 6: ON CONFLICT (basic and advanced)
1963        assert!(parse("POST", "users", "on_conflict=email", Some(body), None).is_ok());
1964
1965        println!("✅ 100% PostgREST Parity Achieved!");
1966    }
1967
1968    #[cfg(any(feature = "postgres", feature = "wasm"))]
1969    mod operation_to_sql_with_cache_tests {
1970        use super::*;
1971        use crate::schema_cache::ForeignKey;
1972
1973        #[test]
1974        fn test_with_cache_none_matches_without_cache() {
1975            let op = parse("GET", "users", "id=eq.1", None, None).unwrap();
1976
1977            let with_cache = operation_to_sql_with_cache("users", &op, None).unwrap();
1978            let without_cache = operation_to_sql("users", &op).unwrap();
1979
1980            assert_eq!(with_cache.query, without_cache.query);
1981            assert_eq!(with_cache.params, without_cache.params);
1982            assert_eq!(with_cache.tables, without_cache.tables);
1983        }
1984
1985        #[test]
1986        fn test_with_cache_select() {
1987            let cache = std::sync::Arc::new(
1988                crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
1989            );
1990            let op = parse("GET", "users", "age=gte.18&limit=5", None, None).unwrap();
1991            let result =
1992                operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
1993
1994            assert!(result.query.contains("SELECT"));
1995            assert!(result.query.contains("WHERE"));
1996            assert!(result.query.contains("LIMIT"));
1997        }
1998
1999        #[test]
2000        fn test_with_cache_insert() {
2001            let cache = std::sync::Arc::new(
2002                crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
2003            );
2004            let body = r#"{"name":"Alice"}"#;
2005            let op = parse("POST", "users", "", Some(body), None).unwrap();
2006            let result =
2007                operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
2008
2009            assert!(result.query.contains("INSERT"));
2010        }
2011
2012        #[test]
2013        fn test_with_cache_update() {
2014            let cache = std::sync::Arc::new(
2015                crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
2016            );
2017            let body = r#"{"status":"active"}"#;
2018            let op = parse("PATCH", "users", "id=eq.1", Some(body), None).unwrap();
2019            let result =
2020                operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
2021
2022            assert!(result.query.contains("UPDATE"));
2023        }
2024
2025        #[test]
2026        fn test_with_cache_delete() {
2027            let cache = std::sync::Arc::new(
2028                crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
2029            );
2030            let op = parse("DELETE", "users", "id=eq.1", None, None).unwrap();
2031            let result =
2032                operation_to_sql_with_cache("users", &op, Some(cache)).unwrap();
2033
2034            assert!(result.query.contains("DELETE"));
2035        }
2036
2037        #[test]
2038        fn test_with_cache_rpc() {
2039            let cache = std::sync::Arc::new(
2040                crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
2041            );
2042            let body = r#"{"user_id": 1}"#;
2043            let op =
2044                parse("POST", "rpc/get_profile", "", Some(body), None).unwrap();
2045            let result = operation_to_sql_with_cache(
2046                "rpc/get_profile",
2047                &op,
2048                Some(cache),
2049            )
2050            .unwrap();
2051
2052            assert!(result.query.contains("get_profile"));
2053        }
2054
2055        #[test]
2056        fn test_with_cache_empty_table_errors() {
2057            let op = parse("GET", "users", "id=eq.1", None, None).unwrap();
2058            let result = operation_to_sql_with_cache("", &op, None);
2059            assert!(result.is_err());
2060        }
2061
2062        #[test]
2063        fn test_with_cache_resolves_many_to_one_relation() {
2064            let fks = vec![ForeignKey::test("orders", "customer_id", "customers", "id")];
2065            let cache = std::sync::Arc::new(
2066                crate::schema_cache::SchemaCache::from_foreign_keys(fks),
2067            );
2068
2069            // select=id,customers(name) on orders table — Many-to-One embed
2070            let op = parse(
2071                "GET",
2072                "orders",
2073                "select=id,customers(name)",
2074                None,
2075                None,
2076            )
2077            .unwrap();
2078            let result =
2079                operation_to_sql_with_cache("orders", &op, Some(cache)).unwrap();
2080
2081            // Should produce a correlated subquery for the M2O relation
2082            assert!(result.query.contains("customers"));
2083            assert!(result.query.contains("customer_id"));
2084        }
2085
2086        #[test]
2087        fn test_with_cache_resolves_one_to_many_relation() {
2088            let fks = vec![ForeignKey::test("orders", "customer_id", "customers", "id")];
2089            let cache = std::sync::Arc::new(
2090                crate::schema_cache::SchemaCache::from_foreign_keys(fks),
2091            );
2092
2093            // select=id,orders(id,total) on customers table — One-to-Many embed
2094            let op = parse(
2095                "GET",
2096                "customers",
2097                "select=id,orders(id)",
2098                None,
2099                None,
2100            )
2101            .unwrap();
2102            let result =
2103                operation_to_sql_with_cache("customers", &op, Some(cache))
2104                    .unwrap();
2105
2106            // Should produce a json_agg subquery for the O2M relation
2107            assert!(result.query.contains("orders"));
2108            assert!(result.query.contains("json_agg"));
2109        }
2110
2111        #[test]
2112        fn test_with_cache_relation_not_found_errors() {
2113            let cache = std::sync::Arc::new(
2114                crate::schema_cache::SchemaCache::from_foreign_keys(vec![]),
2115            );
2116
2117            // Try to embed a relation that doesn't exist in the cache
2118            let op = parse(
2119                "GET",
2120                "orders",
2121                "select=id,nonexistent(name)",
2122                None,
2123                None,
2124            )
2125            .unwrap();
2126            let result =
2127                operation_to_sql_with_cache("orders", &op, Some(cache));
2128
2129            assert!(result.is_err());
2130        }
2131
2132        #[test]
2133        fn test_without_cache_relation_uses_placeholder() {
2134            // Without a cache, relations produce placeholder SQL
2135            let op = parse(
2136                "GET",
2137                "orders",
2138                "select=id,customers(name)",
2139                None,
2140                None,
2141            )
2142            .unwrap();
2143            let result = operation_to_sql_with_cache("orders", &op, None).unwrap();
2144
2145            // Should still produce SQL (placeholder mode), not error
2146            assert!(result.query.contains("SELECT"));
2147        }
2148    }
2149}