use sqlparser::ast::Statement;
use crate::ast::SqltStatement;
use crate::dialect::DialectId;
use crate::dialect::caps::{DialectCaps, caps_for};
use crate::lint::ctx::LintCtx;
use crate::lint::diagnostic::Diagnostic;
use crate::lint::rule::{Category, Rule, RuleId, RuleMeta, Severity};
pub struct PreflightReturningUnsupported;
const META_PF_RETURNING: RuleMeta = RuleMeta {
id: RuleId("SQLT0200"),
name: "preflight-returning-unsupported",
category: Category::PreFlight,
default_severity: Severity::Warning,
default_enabled: true,
summary: "Target dialect does not support RETURNING on this DML statement.",
explanation: "MySQL and MSSQL lack RETURNING. `sqlt translate` will drop the clause; under \
`--strict` the translation fails. If the surrounding code consumes the returned \
rows, the application will need to fetch them with a separate SELECT.",
};
impl Rule for PreflightReturningUnsupported {
fn meta(&self) -> &'static RuleMeta {
&META_PF_RETURNING
}
fn check_statement(&self, stmt: &SqltStatement, ctx: &LintCtx, out: &mut Vec<Diagnostic>) {
let Some(dst) = ctx.dst else {
return;
};
let dst_caps = caps_for(dst);
let SqltStatement::Std(boxed) = stmt else {
return;
};
let unsupported = match &**boxed {
Statement::Insert(i) => i.returning.is_some() && !dst_caps.returning_in_insert,
Statement::Update { returning, .. } => {
returning.is_some() && !dst_caps.returning_in_update
}
Statement::Delete(d) => d.returning.is_some() && !dst_caps.returning_in_delete,
_ => false,
};
if unsupported {
out.push(diag(
&META_PF_RETURNING,
ctx,
&format!("RETURNING is not supported by target dialect {dst}"),
Some(
"`sqlt translate` will drop the clause; fetch rows with a follow-up SELECT"
.into(),
),
));
}
}
}
pub struct PreflightOnDuplicateUnsupported;
const META_PF_ONDUP: RuleMeta = RuleMeta {
id: RuleId("SQLT0201"),
name: "preflight-on-duplicate-unsupported",
category: Category::PreFlight,
default_severity: Severity::Error,
default_enabled: true,
summary: "Target dialect does not support `ON DUPLICATE KEY UPDATE`.",
explanation: "MySQL/MariaDB-only. Postgres and SQLite use `ON CONFLICT (...) DO UPDATE SET ...`; \
MSSQL uses `MERGE`. `sqlt translate` rewrites the simple cases.",
};
impl Rule for PreflightOnDuplicateUnsupported {
fn meta(&self) -> &'static RuleMeta {
&META_PF_ONDUP
}
fn check_statement(&self, stmt: &SqltStatement, ctx: &LintCtx, out: &mut Vec<Diagnostic>) {
let Some(dst) = ctx.dst else {
return;
};
let dst_caps = caps_for(dst);
if dst_caps.on_duplicate_key_update {
return;
}
let SqltStatement::Std(boxed) = stmt else {
return;
};
let Statement::Insert(i) = &**boxed else {
return;
};
if matches!(&i.on, Some(sqlparser::ast::OnInsert::DuplicateKeyUpdate(_))) {
let suggestion = if dst_caps.on_conflict {
"rewrite as `ON CONFLICT (col) DO UPDATE SET ...`"
} else {
"rewrite as MERGE for MSSQL, or restructure as separate INSERT + UPDATE"
};
out.push(diag(
&META_PF_ONDUP,
ctx,
&format!("ON DUPLICATE KEY UPDATE is not supported by {dst}"),
Some(suggestion.into()),
));
}
}
}
pub struct PreflightOnConflictUnsupported;
const META_PF_ONCONFLICT: RuleMeta = RuleMeta {
id: RuleId("SQLT0202"),
name: "preflight-on-conflict-unsupported",
category: Category::PreFlight,
default_severity: Severity::Error,
default_enabled: true,
summary: "Target dialect does not support `ON CONFLICT`.",
explanation: "Postgres/SQLite-specific. MySQL/MariaDB use `ON DUPLICATE KEY UPDATE`; MSSQL \
uses `MERGE`. `sqlt translate` rewrites the simple cases.",
};
impl Rule for PreflightOnConflictUnsupported {
fn meta(&self) -> &'static RuleMeta {
&META_PF_ONCONFLICT
}
fn check_statement(&self, stmt: &SqltStatement, ctx: &LintCtx, out: &mut Vec<Diagnostic>) {
let Some(dst) = ctx.dst else {
return;
};
let dst_caps = caps_for(dst);
if dst_caps.on_conflict {
return;
}
let SqltStatement::Std(boxed) = stmt else {
return;
};
let Statement::Insert(i) = &**boxed else {
return;
};
if matches!(&i.on, Some(sqlparser::ast::OnInsert::OnConflict(_))) {
let suggestion = if dst_caps.on_duplicate_key_update {
"rewrite as `ON DUPLICATE KEY UPDATE`"
} else {
"rewrite as MERGE for MSSQL"
};
out.push(diag(
&META_PF_ONCONFLICT,
ctx,
&format!("ON CONFLICT is not supported by {dst}"),
Some(suggestion.into()),
));
}
}
}
pub struct PreflightCreateSequenceUnsupported;
const META_PF_SEQ: RuleMeta = RuleMeta {
id: RuleId("SQLT0203"),
name: "preflight-create-sequence-unsupported",
category: Category::PreFlight,
default_severity: Severity::Warning,
default_enabled: true,
summary: "Target dialect does not support `CREATE SEQUENCE`.",
explanation: "MySQL and SQLite lack named sequences. Use `AUTO_INCREMENT` / `AUTOINCREMENT` \
on the column, or maintain a counter table by hand. `sqlt translate` will warn \
and emit the original SQL verbatim.",
};
impl Rule for PreflightCreateSequenceUnsupported {
fn meta(&self) -> &'static RuleMeta {
&META_PF_SEQ
}
fn check_statement(&self, stmt: &SqltStatement, ctx: &LintCtx, out: &mut Vec<Diagnostic>) {
let Some(dst) = ctx.dst else {
return;
};
if caps_for(dst).create_sequence {
return;
}
let SqltStatement::Std(boxed) = stmt else {
return;
};
if matches!(&**boxed, Statement::CreateSequence { .. }) {
out.push(diag(
&META_PF_SEQ,
ctx,
&format!("CREATE SEQUENCE is not supported by {dst}"),
Some("use AUTO_INCREMENT/AUTOINCREMENT on the column or a counter table".into()),
));
}
}
}
pub struct PreflightRawPassthroughUnsupported;
const META_PF_RAW: RuleMeta = RuleMeta {
id: RuleId("SQLT0204"),
name: "preflight-raw-passthrough-unsupported",
category: Category::PreFlight,
default_severity: Severity::Error,
default_enabled: true,
summary: "MariaDB-specific construct cannot be represented in non-MariaDB targets.",
explanation: "Constructs like WITH SYSTEM VERSIONING, FOR SYSTEM_TIME, CREATE PACKAGE, and \
vector types fall back to raw text in v1. `sqlt translate` will emit the original \
SQL with a RAW_PASSTHROUGH warning; the target server will reject it.",
};
impl Rule for PreflightRawPassthroughUnsupported {
fn meta(&self) -> &'static RuleMeta {
&META_PF_RAW
}
fn check_statement(&self, stmt: &SqltStatement, ctx: &LintCtx, out: &mut Vec<Diagnostic>) {
let Some(dst) = ctx.dst else {
return;
};
if caps_for(dst).mariadb_raw_native {
return;
}
let SqltStatement::Raw(r) = stmt else {
return;
};
out.push(diag(
&META_PF_RAW,
ctx,
&format!(
"raw {} fragment cannot be represented in {dst}; emitted SQL will be rejected by the target server",
r.reason
),
Some("rewrite the construct as standard SQL, or keep --to as mariadb".into()),
));
}
}
pub struct PreflightQuoteStyleMismatch;
const META_PF_QUOTE: RuleMeta = RuleMeta {
id: RuleId("SQLT0205"),
name: "preflight-quote-style-mismatch",
category: Category::PreFlight,
default_severity: Severity::Info,
default_enabled: false,
summary: "Identifier quote style won't survive emit to the target dialect.",
explanation: "Disabled by default — the emitter renders identifiers using the upstream \
Display impl, which is generally faithful for the target dialect. Opt-in for \
cases where you want every quote-style change flagged for review.",
};
impl Rule for PreflightQuoteStyleMismatch {
fn meta(&self) -> &'static RuleMeta {
&META_PF_QUOTE
}
}
fn diag(
meta: &'static RuleMeta,
ctx: &LintCtx,
msg: &str,
suggestion: Option<String>,
) -> Diagnostic {
Diagnostic {
rule: meta.id,
rule_name: meta.name,
category: meta.category,
severity: meta.default_severity,
message: msg.to_string(),
suggestion,
span: ctx.stmt_span,
stmt_index: ctx.stmt_index,
source_dialect: ctx.src,
target_dialect: ctx.dst,
}
}
#[allow(dead_code)]
fn _force_caps_used(_: DialectCaps, _: DialectId) {}