use std::fmt::{self, Write as _};
pub use crate::ast::render::{
FragmentRender, RenderConfig, RenderError, RenderErrorKind, RenderMode, RenderResult,
RenderSpelling,
};
use crate::ast::dialect::FeatureSet;
use crate::ast::generated::visit::{
Visit, walk_data_type, walk_expr, walk_join_constraint, walk_on_conflict, walk_query,
walk_returning, walk_statement, walk_table_factor, walk_upsert,
};
use crate::ast::render::{Render, RenderCtx, RenderExt};
use crate::ast::{
DataType, DerivedSpelling, Expr, Extension, JoinConstraint, NoExt, NodeId, OnConflict,
ParameterKind, ParameterSigil, Query, RelationInheritance, Resolver, Returning,
SessionVariableKind, SourceStore, Span, Statement, TableFactor, Upsert,
};
use crate::error::ParseError;
use crate::parser::{Dialect, Parsed};
pub trait RenderDialect {
fn render_features(&self) -> FeatureSet {
FeatureSet::ANSI
}
fn validate_statement(&self, statement: &Statement<NoExt>) -> RenderResult<()> {
validate_target_support(&self.render_features(), statement)
}
}
fn validate_target_support(
features: &FeatureSet,
statement: &Statement<NoExt>,
) -> RenderResult<()> {
let mut support = TargetSupport {
features,
rejection: None,
};
support.visit_statement(statement);
match support.rejection {
Some(error) => Err(error),
None => Ok(()),
}
}
struct TargetSupport<'a> {
features: &'a FeatureSet,
rejection: Option<RenderError>,
}
impl TargetSupport<'_> {
fn reject(&mut self, span: Span, message: &str) {
if self.rejection.is_none() {
self.rejection = Some(RenderError::unsupported(Some(span), message));
}
}
}
impl<'ast> Visit<'ast> for TargetSupport<'_> {
fn visit_table_factor(&mut self, node: &'ast TableFactor<NoExt>) {
if self.rejection.is_some() {
return;
}
let exprs = &self.features.table_expressions;
let factors = &self.features.table_factor_syntax;
match node {
TableFactor::Table {
inheritance,
sample,
meta,
..
} => {
if matches!(inheritance, RelationInheritance::Only(_)) && !exprs.only {
self.reject(
meta.span,
"target does not support ONLY inheritance suppression",
);
} else if matches!(inheritance, RelationInheritance::Descendants) && !exprs.only {
self.reject(
meta.span,
"target does not support the descendant-table `*` marker",
);
} else if sample.is_some() && !exprs.table_sample {
self.reject(meta.span, "target does not support TABLESAMPLE");
}
}
TableFactor::Derived {
lateral,
spelling,
meta,
..
} => {
if *lateral && !factors.lateral {
self.reject(meta.span, "target does not support LATERAL derived tables");
} else if matches!(spelling, DerivedSpelling::BareValues) && !factors.from_values {
self.reject(
meta.span,
"target does not support bare `FROM VALUES` table factors",
);
}
}
TableFactor::Function {
lateral,
with_ordinality,
meta,
..
} => {
if !factors.table_functions {
self.reject(meta.span, "target does not support table functions in FROM");
} else if *with_ordinality && !factors.table_function_ordinality {
self.reject(meta.span, "target does not support WITH ORDINALITY");
} else if *lateral && !factors.lateral {
self.reject(meta.span, "target does not support LATERAL table functions");
}
}
TableFactor::RowsFrom {
lateral,
with_ordinality,
meta,
..
} => {
if !factors.rows_from {
self.reject(
meta.span,
"target does not support ROWS FROM table functions",
);
} else if *with_ordinality && !factors.table_function_ordinality {
self.reject(meta.span, "target does not support WITH ORDINALITY");
} else if *lateral && !factors.lateral {
self.reject(meta.span, "target does not support LATERAL table functions");
}
}
TableFactor::Unnest {
lateral,
with_offset,
with_ordinality,
meta,
..
} => {
if !factors.unnest {
self.reject(meta.span, "target does not support the UNNEST table factor");
} else if *with_ordinality && !factors.table_function_ordinality {
self.reject(meta.span, "target does not support WITH ORDINALITY");
} else if *with_offset && !factors.unnest_with_offset {
self.reject(meta.span, "target does not support UNNEST ... WITH OFFSET");
} else if *lateral && !factors.lateral {
self.reject(meta.span, "target does not support LATERAL table functions");
}
}
TableFactor::Pivot { meta, .. } => {
if !factors.pivot {
self.reject(meta.span, "target does not support the PIVOT operator");
}
}
TableFactor::Unpivot { meta, .. } => {
if !factors.unpivot {
self.reject(meta.span, "target does not support the UNPIVOT operator");
}
}
TableFactor::MatchRecognize { meta, .. } => {
if !factors.match_recognize {
self.reject(
meta.span,
"target does not support the MATCH_RECOGNIZE table factor",
);
}
}
TableFactor::ShowRef { meta, .. } => {
if !factors.show_ref {
self.reject(
meta.span,
"target does not support DESCRIBE/SHOW/SUMMARIZE as a table source",
);
}
}
TableFactor::JsonTable {
json_table, meta, ..
} => {
if !factors.json_table {
self.reject(
meta.span,
"target does not support the JSON_TABLE table factor",
);
} else if json_table.lateral && !factors.lateral {
self.reject(meta.span, "target does not support LATERAL table functions");
}
}
TableFactor::XmlTable {
xml_table, meta, ..
} => {
if !factors.xml_table {
self.reject(
meta.span,
"target does not support the XMLTABLE table factor",
);
} else if xml_table.lateral && !factors.lateral {
self.reject(meta.span, "target does not support LATERAL table functions");
}
}
TableFactor::OpenJson { meta, .. } => {
if !factors.open_json {
self.reject(
meta.span,
"target does not support the OPENJSON table factor",
);
}
}
TableFactor::TableExpr { meta, .. } => {
if !factors.table_expr_factor {
self.reject(
meta.span,
"target does not support the TABLE(<expr>) table factor",
);
}
}
TableFactor::SpecialFunction { .. }
| TableFactor::NestedJoin { .. }
| TableFactor::Other { .. } => {}
}
if self.rejection.is_some() {
return;
}
walk_table_factor(self, node);
}
fn visit_join_constraint(&mut self, node: &'ast JoinConstraint<NoExt>) {
if self.rejection.is_some() {
return;
}
if let JoinConstraint::Using {
alias: Some(_),
meta,
..
} = node
{
if !self.features.table_expressions.join_using_alias {
self.reject(
meta.span,
"target does not support JOIN ... USING (...) AS alias",
);
}
}
if self.rejection.is_some() {
return;
}
walk_join_constraint(self, node);
}
fn visit_expr(&mut self, node: &'ast Expr<NoExt>) {
if self.rejection.is_some() {
return;
}
if let Expr::Parameter { kind, meta } = node {
let params = &self.features.parameters;
let (supported, message) = match kind {
ParameterKind::Positional(_) | ParameterKind::PositionalLarge { .. } => (
params.positional_dollar,
"target does not support positional $n parameters",
),
ParameterKind::Numbered(_) => (
params.numbered_question,
"target does not support numbered ?n parameters",
),
ParameterKind::Anonymous => (
params.anonymous_question,
"target does not support anonymous ? parameters",
),
ParameterKind::Named {
sigil: ParameterSigil::Colon,
..
} => (
params.named_colon,
"target does not support named :name parameters",
),
ParameterKind::Named {
sigil: ParameterSigil::At,
..
} => (
params.named_at,
"target does not support named @name parameters",
),
ParameterKind::Named {
sigil: ParameterSigil::Dollar,
..
} => (
params.named_dollar,
"target does not support named $name parameters",
),
};
if !supported {
self.reject(meta.span, message);
}
}
if let Expr::SessionVariable { kind, meta, .. } = node {
let vars = &self.features.session_variables;
let (supported, message) = match kind {
SessionVariableKind::User => (
vars.user_variables,
"target does not support @name user variables",
),
SessionVariableKind::System
| SessionVariableKind::SystemGlobal
| SessionVariableKind::SystemSession => (
vars.system_variables,
"target does not support @@name system variables",
),
};
if !supported {
self.reject(meta.span, message);
}
}
if self.rejection.is_some() {
return;
}
walk_expr(self, node);
}
fn visit_returning(&mut self, node: &'ast Returning<NoExt>) {
if self.rejection.is_some() {
return;
}
if !self.features.mutation_syntax.returning {
self.reject(node.meta.span, "target does not support RETURNING");
return;
}
walk_returning(self, node);
}
fn visit_upsert(&mut self, node: &'ast Upsert<NoExt>) {
if self.rejection.is_some() {
return;
}
if let Upsert::OnDuplicateKeyUpdate { meta, .. } = node {
if !self.features.mutation_syntax.on_duplicate_key_update {
self.reject(meta.span, "target does not support ON DUPLICATE KEY UPDATE");
return;
}
}
walk_upsert(self, node);
}
fn visit_on_conflict(&mut self, node: &'ast OnConflict<NoExt>) {
if self.rejection.is_some() {
return;
}
if !self.features.mutation_syntax.on_conflict {
self.reject(node.meta.span, "target does not support ON CONFLICT");
return;
}
walk_on_conflict(self, node);
}
}
#[derive(Clone, Debug)]
pub struct Renderer<D> {
dialect: D,
config: RenderConfig,
}
impl<D: RenderDialect> Renderer<D> {
#[must_use]
pub fn new(dialect: D) -> Self {
Self::with_config(dialect, RenderConfig::default())
}
#[must_use]
pub fn with_config(dialect: D, mut config: RenderConfig) -> Self {
config.target = dialect.render_features();
config.spelling = RenderSpelling::TargetDialect;
Self { dialect, config }
}
pub fn dialect(&self) -> &D {
&self.dialect
}
pub fn config(&self) -> &RenderConfig {
&self.config
}
pub fn render_parsed<S: SourceStore>(&self, parsed: &Parsed<S>) -> RenderResult<String> {
let mut out = String::with_capacity(parsed.source().len());
for (i, statement) in parsed.statements().iter().enumerate() {
if i > 0 {
out.push_str("; ");
}
self.render_statement_into(statement, parsed.resolver(), parsed.source(), &mut out)?;
}
Ok(out)
}
pub fn render_statement(
&self,
statement: &Statement<NoExt>,
resolver: &dyn Resolver,
source: &str,
) -> RenderResult<String> {
let mut out = String::with_capacity(source.len());
self.render_statement_into(statement, resolver, source, &mut out)?;
Ok(out)
}
fn render_statement_into(
&self,
statement: &Statement<NoExt>,
resolver: &dyn Resolver,
source: &str,
out: &mut String,
) -> RenderResult<()> {
self.dialect.validate_statement(statement)?;
let ctx = RenderCtx::new(resolver, source, &self.config);
write!(out, "{}", statement.displayed(&ctx)).map_err(RenderError::from)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TranspileError {
Parse(ParseError),
Render(RenderError),
}
impl fmt::Display for TranspileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Parse(error) => write!(f, "transpile could not parse the source: {error}"),
Self::Render(error) => write!(f, "transpile could not render for the target: {error}"),
}
}
}
impl std::error::Error for TranspileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(error) => Some(error),
Self::Render(error) => Some(error),
}
}
}
impl From<ParseError> for TranspileError {
fn from(error: ParseError) -> Self {
Self::Parse(error)
}
}
impl From<RenderError> for TranspileError {
fn from(error: RenderError) -> Self {
Self::Render(error)
}
}
pub fn transpile<Src, Tgt>(sql: &str, source: Src, target: Tgt) -> Result<String, TranspileError>
where
Src: Dialect<Ext = NoExt>,
Tgt: RenderDialect,
{
let parsed = crate::parse_with(sql, crate::ParseConfig::new(source))?;
Renderer::new(target)
.render_parsed(&parsed)
.map_err(Into::into)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FragmentError {
node_id: NodeId,
}
impl FragmentError {
pub fn node_id(&self) -> NodeId {
self.node_id
}
}
impl fmt::Display for FragmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"no standalone-renderable node with id {}: the fragment API renders only \
complete expressions, queries, statements, and data types; the id may be \
absent or may name a context-dependent node (such as a join constraint, \
select item, or order-by term) that cannot render on its own",
self.node_id.as_u32(),
)
}
}
impl std::error::Error for FragmentError {}
impl<S: SourceStore, X: Extension + Render> Parsed<S, X> {
pub fn render_fragment<N: FragmentRender>(&self, node: &N) -> String {
self.render_fragment_with(node, &RenderConfig::default())
}
pub fn render_fragment_with<N: FragmentRender>(
&self,
node: &N,
config: &RenderConfig,
) -> String {
let ctx = RenderCtx::new(self.resolver(), self.source(), config);
let mut out = String::new();
write!(out, "{}", node.displayed(&ctx))
.expect("rendering a fragment to a String cannot fail");
out
}
pub fn render_fragment_by_id(
&self,
node_id: NodeId,
config: &RenderConfig,
) -> Result<String, FragmentError> {
let mut finder = FragmentFinder {
resolver: self.resolver(),
source: self.source(),
config,
target: node_id,
rendered: None,
};
for statement in self.statements() {
if finder.rendered.is_some() {
break;
}
finder.visit_statement(statement);
}
finder.rendered.ok_or(FragmentError { node_id })
}
}
struct FragmentFinder<'a> {
resolver: &'a dyn Resolver,
source: &'a str,
config: &'a RenderConfig,
target: NodeId,
rendered: Option<String>,
}
impl FragmentFinder<'_> {
fn take<N: FragmentRender>(&mut self, node: &N) {
let ctx = RenderCtx::new(self.resolver, self.source, self.config);
let mut out = String::new();
write!(out, "{}", node.displayed(&ctx))
.expect("rendering a fragment to a String cannot fail");
self.rendered = Some(out);
}
}
impl<'ast, X: Extension + Render> Visit<'ast, X> for FragmentFinder<'_> {
fn visit_statement(&mut self, node: &'ast Statement<X>) {
if self.rendered.is_some() {
return;
}
if node.fragment_node_id() == self.target {
self.take(node);
return;
}
walk_statement(self, node);
}
fn visit_query(&mut self, node: &'ast Query<X>) {
if self.rendered.is_some() {
return;
}
if node.fragment_node_id() == self.target {
self.take(node);
return;
}
walk_query(self, node);
}
fn visit_expr(&mut self, node: &'ast Expr<X>) {
if self.rendered.is_some() {
return;
}
if node.fragment_node_id() == self.target {
self.take(node);
return;
}
walk_expr(self, node);
}
fn visit_data_type(&mut self, node: &'ast DataType<X>) {
if self.rendered.is_some() {
return;
}
if node.fragment_node_id() == self.target {
self.take(node);
return;
}
walk_data_type(self, node);
}
}
#[cfg(test)]
mod fragment_tests {
use super::*;
use crate::ast::render::RenderMode;
use crate::ast::{SelectItem, SetExpr};
use crate::dialect::Ansi;
use crate::parse_with;
use std::sync::Arc;
fn first_projection_expr<X: Extension>(parsed: &Parsed<Arc<str>, X>) -> &Expr<X> {
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let SelectItem::Expr { expr, .. } = &select.projection[0] else {
panic!("expected an expression projection");
};
expr
}
#[test]
fn render_fragment_renders_an_expression_standalone() {
let parsed = parse_with("SELECT a + 1", crate::ParseConfig::new(Ansi)).expect("parses");
let expr = first_projection_expr(&parsed);
assert_eq!(parsed.render_fragment(expr), "a + 1");
}
#[test]
fn render_fragment_by_id_matches_the_by_reference_path() {
let parsed = parse_with(
"SELECT a + 1 FROM t WHERE b > 2",
crate::ParseConfig::new(Ansi),
)
.expect("parses");
let expr = first_projection_expr(&parsed);
let id = expr.fragment_node_id();
let by_ref = parsed.render_fragment(expr);
let by_id = parsed
.render_fragment_by_id(id, &RenderConfig::default())
.expect("the expression id resolves");
assert_eq!(by_ref, by_id);
assert_eq!(by_id, "a + 1");
}
#[test]
fn render_fragment_by_id_renders_a_whole_query_and_statement() {
let parsed = parse_with(
"SELECT a FROM t WHERE b IN (SELECT c FROM u)",
crate::ParseConfig::new(Ansi),
)
.expect("parses");
let Statement::Query { query, meta, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
assert_eq!(
parsed
.render_fragment_by_id(meta.node_id, &RenderConfig::default())
.expect("statement id resolves"),
"SELECT a FROM t WHERE b IN (SELECT c FROM u)",
);
assert_eq!(
parsed
.render_fragment_by_id(query.meta.node_id, &RenderConfig::default())
.expect("query id resolves"),
"SELECT a FROM t WHERE b IN (SELECT c FROM u)",
);
}
#[test]
fn render_fragment_by_id_rejects_a_context_dependent_node() {
let parsed = parse_with("SELECT a FROM t", crate::ParseConfig::new(Ansi)).expect("parses");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let error = parsed
.render_fragment_by_id(select.meta.node_id, &RenderConfig::default())
.expect_err("a Select node is not standalone-renderable");
assert_eq!(error.node_id(), select.meta.node_id);
assert!(error.to_string().contains("standalone-renderable"));
}
#[test]
fn render_fragment_by_id_rejects_an_absent_id() {
let parsed = parse_with("SELECT 1", crate::ParseConfig::new(Ansi)).expect("parses");
let absent = NodeId::new(u32::MAX).expect("non-zero id");
parsed
.render_fragment_by_id(absent, &RenderConfig::default())
.expect_err("no node carries this id");
}
#[test]
fn render_fragment_with_honours_redacted_mode() {
let parsed =
parse_with("SELECT secret + 42", crate::ParseConfig::new(Ansi)).expect("parses");
let expr = first_projection_expr(&parsed);
let config = RenderConfig {
mode: RenderMode::Redacted,
..RenderConfig::default()
};
assert_eq!(parsed.render_fragment_with(expr, &config), "id + ?");
}
#[test]
fn expression_fragment_reparses_inside_a_select() {
let parsed = parse_with("SELECT (a + 1) * b - c / 2", crate::ParseConfig::new(Ansi))
.expect("parses");
let expr = first_projection_expr(&parsed);
let fragment = parsed.render_fragment(expr);
let reparsed = parse_with(&format!("SELECT {fragment}"), crate::ParseConfig::new(Ansi))
.expect("the expression fragment re-parses in projection position");
let reparsed_expr = first_projection_expr(&reparsed);
assert_eq!(reparsed.render_fragment(reparsed_expr), fragment);
}
#[test]
fn query_fragment_reparses_as_a_statement() {
let parsed = parse_with(
"SELECT a FROM t WHERE b IN (SELECT c FROM u WHERE d > 1)",
crate::ParseConfig::new(Ansi),
)
.expect("parses");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let Some(Expr::InSubquery { subquery, .. }) = &select.selection else {
panic!("expected an IN (subquery) predicate");
};
let fragment = parsed.render_fragment(subquery.as_ref());
assert_eq!(fragment, "SELECT c FROM u WHERE d > 1");
let reparsed = parse_with(&fragment, crate::ParseConfig::new(Ansi))
.expect("the query fragment re-parses as a standalone statement");
assert_eq!(reparsed.to_sql(), fragment);
}
#[test]
fn data_type_fragment_reparses_inside_a_cast() {
let parsed = parse_with(
"SELECT CAST(x AS DECIMAL(10, 2))",
crate::ParseConfig::new(Ansi),
)
.expect("parses");
let expr = first_projection_expr(&parsed);
let Expr::Cast { data_type, .. } = expr else {
panic!("expected a CAST expression");
};
let fragment = parsed.render_fragment(data_type.as_ref());
assert_eq!(fragment, "DECIMAL(10, 2)");
let reparsed = parse_with(
&format!("SELECT CAST(x AS {fragment})"),
crate::ParseConfig::new(Ansi),
)
.expect("the data-type fragment re-parses in a CAST");
let reparsed_expr = first_projection_expr(&reparsed);
let Expr::Cast { data_type, .. } = reparsed_expr else {
panic!("expected a CAST expression");
};
assert_eq!(reparsed.render_fragment(data_type.as_ref()), fragment);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::dialect::{FeatureDelta, OperatorSyntax, ParameterSyntax};
use crate::ast::{CteBody, Query, SetExpr, Span};
use crate::dialect::{Ansi, Postgres, parse};
use crate::parse_with;
use crate::parser::FeatureDialect;
#[test]
fn stock_renderer_delegates_to_tier1_canonical_rendering() {
let parsed = parse("SELECT TRUE, NULL").expect("query parses");
let rendered = Renderer::new(Ansi)
.render_parsed(&parsed)
.expect("stock ANSI target renders");
assert_eq!(rendered, "SELECT TRUE, NULL");
}
#[test]
fn renderer_keeps_tier1_modes_after_target_validation() {
let parsed = parse("SELECT TRUE, NULL").expect("query parses");
let renderer = Renderer::with_config(
Ansi,
RenderConfig {
mode: RenderMode::Redacted,
..RenderConfig::default()
},
);
assert_eq!(
renderer.render_parsed(&parsed).expect("redacted renders"),
"SELECT ?, ?",
);
}
#[test]
fn renderer_uses_dialect_target_type_spellings() {
assert_eq!(Renderer::new(Ansi).config().target, FeatureSet::ANSI);
assert_eq!(
Renderer::new(Postgres).config().target,
FeatureSet::POSTGRES
);
assert_eq!(
Renderer::new(Postgres).config().spelling,
RenderSpelling::TargetDialect,
);
let ansi = parse("SELECT CAST(a AS VARCHAR(5))").expect("ANSI cast parses");
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&ansi)
.expect("ANSI target renders"),
"SELECT CAST(a AS CHARACTER VARYING(5))",
);
let postgres = crate::parse_with(
"SELECT CAST(a AS TIMESTAMP(3) WITH TIME ZONE)",
crate::ParseConfig::new(Postgres),
)
.expect("PostgreSQL cast parses");
assert_eq!(
Renderer::new(Postgres)
.render_parsed(&postgres)
.expect("PostgreSQL target renders"),
"SELECT CAST(a AS TIMESTAMPTZ(3))",
);
}
#[test]
fn renderer_handles_create_table_statements() {
let parsed = parse("CREATE TABLE t (id INT PRIMARY KEY, name TEXT NOT NULL DEFAULT 'x')")
.expect("CREATE TABLE parses");
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&parsed)
.expect("CREATE TABLE renders"),
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL DEFAULT 'x')",
);
}
#[test]
fn renderer_handles_create_table_as_select() {
let parsed =
parse("CREATE TEMP TABLE IF NOT EXISTS t (id) ON COMMIT DROP AS SELECT 1 WITH NO DATA")
.expect("CTAS parses");
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&parsed)
.expect("CTAS renders"),
"CREATE TEMP TABLE IF NOT EXISTS t (id) ON COMMIT DROP AS SELECT 1 WITH NO DATA",
);
}
#[test]
fn renderer_handles_insert_statements() {
let parsed = parse(
"WITH src AS (SELECT 1) INSERT INTO t AS target (id) OVERRIDING USER VALUE SELECT * FROM src",
)
.expect("INSERT parses");
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&parsed)
.expect("INSERT renders"),
"WITH src AS (SELECT 1) INSERT INTO t AS target (id) OVERRIDING USER VALUE SELECT * FROM src",
);
}
#[test]
fn renderer_handles_update_and_delete_statements() {
let parsed = parse(
"UPDATE t target SET a = 1, b = DEFAULT FROM u WHERE target.id = u.id; \
WITH src AS (SELECT 1) DELETE FROM t target USING u WHERE target.id = u.id",
)
.expect("UPDATE and DELETE parse");
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&parsed)
.expect("UPDATE and DELETE render"),
"UPDATE t AS target SET a = 1, b = DEFAULT FROM u WHERE target.id = u.id; \
WITH src AS (SELECT 1) DELETE FROM t AS target USING u WHERE target.id = u.id",
);
}
#[test]
fn renderer_handles_returning_and_on_conflict_statements() {
let parsed = parse_with(
"INSERT INTO t (id, n) VALUES (1, 2) ON CONFLICT (id) WHERE id > 0 DO UPDATE SET n = excluded.n WHERE t.n < excluded.n RETURNING id; \
INSERT INTO t VALUES (1) ON CONFLICT ON CONSTRAINT t_pkey DO NOTHING; \
UPDATE t SET a = 1 WHERE id = 2 RETURNING a, id; \
DELETE FROM t WHERE id = 1 RETURNING *",
crate::ParseConfig::new(Postgres),
)
.expect("RETURNING and ON CONFLICT parse under PostgreSQL");
assert_eq!(
Renderer::new(Postgres)
.render_parsed(&parsed)
.expect("RETURNING and ON CONFLICT render"),
"INSERT INTO t (id, n) VALUES (1, 2) ON CONFLICT (id) WHERE id > 0 DO UPDATE SET n = excluded.n WHERE t.n < excluded.n RETURNING id; \
INSERT INTO t VALUES (1) ON CONFLICT ON CONSTRAINT t_pkey DO NOTHING; \
UPDATE t SET a = 1 WHERE id = 2 RETURNING a, id; \
DELETE FROM t WHERE id = 1 RETURNING *",
);
}
#[test]
fn renderer_reports_unsupported_target_constructs() {
#[derive(Clone, Copy, Debug)]
struct NoValuesTarget;
impl RenderDialect for NoValuesTarget {
fn validate_statement(&self, statement: &Statement<NoExt>) -> RenderResult<()> {
if let Some(span) = first_values_span(statement) {
return Err(RenderError::unsupported(
Some(span),
"target does not support VALUES query bodies",
));
}
Ok(())
}
}
let parsed = parse("VALUES (1)").expect("VALUES query parses");
let error = Renderer::new(NoValuesTarget)
.render_parsed(&parsed)
.expect_err("target rejects VALUES");
assert_eq!(error.kind(), RenderErrorKind::Unsupported);
assert_eq!(error.span(), Some(Span::new(0, 10)));
assert!(error.message().contains("VALUES"));
assert_eq!(
error.to_string(),
"target does not support VALUES query bodies at bytes 0..10",
);
}
#[test]
fn ansi_target_rejects_postgres_only_constructs() {
let cases = [
("SELECT * FROM ONLY t", "ONLY"),
("SELECT * FROM t TABLESAMPLE BERNOULLI (10)", "TABLESAMPLE"),
("SELECT * FROM generate_series(1, 5)", "table functions"),
("SELECT * FROM LATERAL (SELECT 1) AS x", "LATERAL"),
("SELECT * FROM t1 JOIN t2 USING (id) AS j", "USING"),
("SELECT $1", "positional"),
("INSERT INTO t VALUES (1) RETURNING id", "RETURNING"),
(
"INSERT INTO t VALUES (1) ON CONFLICT DO NOTHING",
"ON CONFLICT",
),
];
for (sql, needle) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?} parses under PostgreSQL: {err:?}"));
Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("PostgreSQL target renders {sql:?}: {err}"));
let error = Renderer::new(Ansi)
.render_parsed(&parsed)
.expect_err("ANSI target rejects the PostgreSQL-only construct");
assert_eq!(error.kind(), RenderErrorKind::Unsupported, "{sql:?}");
assert!(error.span().is_some(), "{sql:?} carries a span");
assert!(
error.message().contains(needle),
"{sql:?}: {:?} should mention {needle:?}",
error.message(),
);
}
}
#[test]
fn postgres_target_rejects_anonymous_parameter_placeholders() {
const ANON_PARAM: FeatureSet = FeatureSet::POSTGRES.with(
FeatureDelta::EMPTY
.parameters(ParameterSyntax {
anonymous_question: true,
..ParameterSyntax::POSTGRES
})
.operator_syntax(OperatorSyntax {
jsonb_operators: false,
..OperatorSyntax::POSTGRES
}),
);
const ANON_PARAM_DIALECT: FeatureDialect = FeatureDialect {
features: &ANON_PARAM,
};
let parsed = parse_with("SELECT ?", crate::ParseConfig::new(ANON_PARAM_DIALECT))
.expect("anonymous-parameter dialect parses ?");
assert_eq!(
Renderer::new(ANON_PARAM_DIALECT)
.render_parsed(&parsed)
.expect("custom target renders ?"),
"SELECT ?",
);
let error = Renderer::new(Postgres)
.render_parsed(&parsed)
.expect_err("PostgreSQL has no ? placeholder");
assert_eq!(error.kind(), RenderErrorKind::Unsupported);
assert!(
error.message().contains("anonymous"),
"{:?}",
error.message(),
);
}
#[test]
fn transpile_renders_cross_dialect_type_spellings() {
assert_eq!(
transpile("SELECT CAST(a AS VARCHAR(5))", Postgres, Ansi)
.expect("ANSI can spell this cast"),
"SELECT CAST(a AS CHARACTER VARYING(5))",
);
}
#[test]
fn transpile_passes_through_parse_errors_with_their_span() {
let error = transpile("FROM t", Ansi, Ansi).expect_err("FROM is not a statement");
let TranspileError::Parse(parse_error) = error else {
panic!("expected a parse failure, got {error:?}");
};
assert_eq!(parse_error.span, Span::new(0, 4));
}
#[test]
fn transpile_passes_through_render_rejections_with_kind_and_span() {
let error = transpile("SELECT $1", Postgres, Ansi).expect_err("ANSI has no $n");
let TranspileError::Render(render_error) = error else {
panic!("expected a render rejection, got {error:?}");
};
assert_eq!(render_error.kind(), RenderErrorKind::Unsupported);
assert!(render_error.span().is_some());
}
#[test]
fn transpile_joins_multiple_statements_with_semicolons() {
assert_eq!(
transpile("SELECT 1; SELECT 2", Ansi, Ansi).expect("both statements render"),
"SELECT 1; SELECT 2",
);
}
fn first_values_span(statement: &Statement<NoExt>) -> Option<Span> {
first_values_span_query(statement.as_query()?)
}
fn first_values_span_query(query: &Query<NoExt>) -> Option<Span> {
query
.with
.as_ref()
.and_then(|with| {
with.ctes.iter().find_map(|cte| match &cte.body {
CteBody::Query { query, .. } => first_values_span_query(query),
_ => None,
})
})
.or_else(|| first_values_span_set(&query.body))
}
fn first_values_span_set(set: &SetExpr<NoExt>) -> Option<Span> {
match set {
SetExpr::Values { meta, .. } => Some(meta.span),
SetExpr::Query { query, .. } => first_values_span_query(query),
SetExpr::SetOperation { left, right, .. } => {
first_values_span_set(left).or_else(|| first_values_span_set(right))
}
SetExpr::Select { .. } | SetExpr::Pivot { .. } | SetExpr::Unpivot { .. } => None,
}
}
}