use super::dialect::{escape_literal, Dialect};
use super::types::{CanonicalType, TypeCategory, TypeSupport};
pub struct PostgresDialect;
pub fn ddl_type(t: &CanonicalType) -> String {
match t {
CanonicalType::Text => "TEXT".to_string(),
CanonicalType::Varchar(Some(n)) => format!("VARCHAR({})", n),
CanonicalType::Varchar(None) => "TEXT".to_string(),
CanonicalType::Char(Some(n)) => format!("CHAR({})", n),
CanonicalType::Char(None) => "CHAR(1)".to_string(),
CanonicalType::SmallInt => "SMALLINT".to_string(),
CanonicalType::Int => "INTEGER".to_string(),
CanonicalType::BigInt => "BIGINT".to_string(),
CanonicalType::Real => "REAL".to_string(),
CanonicalType::Double => "DOUBLE PRECISION".to_string(),
CanonicalType::Decimal(Some((p, s))) => format!("NUMERIC({}, {})", p, s),
CanonicalType::Decimal(None) => "NUMERIC".to_string(),
CanonicalType::Boolean => "BOOLEAN".to_string(),
CanonicalType::Uuid => "UUID".to_string(),
CanonicalType::Json => "JSONB".to_string(),
CanonicalType::Jsonb => "JSONB".to_string(),
CanonicalType::Timestamp => "TIMESTAMPTZ".to_string(),
CanonicalType::TimestampNtz => "TIMESTAMP".to_string(),
CanonicalType::Date => "DATE".to_string(),
CanonicalType::Time => "TIME".to_string(),
CanonicalType::Timetz => "TIMETZ".to_string(),
CanonicalType::Bytes => "BYTEA".to_string(),
CanonicalType::Serial => "SERIAL".to_string(),
CanonicalType::BigSerial => "BIGSERIAL".to_string(),
CanonicalType::Asset => "TEXT".to_string(),
CanonicalType::AssetArray => "JSONB".to_string(),
CanonicalType::Array(inner) => format!("{}[]", ddl_type(inner)),
CanonicalType::Custom(s) => s.clone(),
}
}
pub fn cast_name(t: &CanonicalType) -> Option<String> {
let s = match t {
CanonicalType::SmallInt => "smallint",
CanonicalType::Int | CanonicalType::Serial => "integer",
CanonicalType::BigInt | CanonicalType::BigSerial => "bigint",
CanonicalType::Real => "real",
CanonicalType::Double => "double precision",
CanonicalType::Decimal(_) => "numeric",
CanonicalType::Boolean => "boolean",
CanonicalType::Uuid => "uuid",
CanonicalType::Json | CanonicalType::Jsonb | CanonicalType::AssetArray => "jsonb",
CanonicalType::Timestamp => "timestamptz",
CanonicalType::TimestampNtz => "timestamp",
CanonicalType::Date => "date",
CanonicalType::Time => "time",
CanonicalType::Timetz => "timetz",
CanonicalType::Bytes => "bytea",
CanonicalType::Array(inner) => {
let inner_cast = cast_name(inner).unwrap_or_else(|| ddl_type(inner).to_lowercase());
return Some(format!("{}[]", inner_cast));
}
CanonicalType::Custom(s) if s.contains('.') => {
return Some(s.clone());
}
CanonicalType::Custom(_) => return None,
_ => return None,
};
Some(s.to_string())
}
pub fn type_support(t: &CanonicalType) -> TypeSupport {
match t {
CanonicalType::Text
| CanonicalType::Varchar(_)
| CanonicalType::Char(_)
| CanonicalType::SmallInt
| CanonicalType::Int
| CanonicalType::BigInt
| CanonicalType::Real
| CanonicalType::Double
| CanonicalType::Decimal(_)
| CanonicalType::Boolean
| CanonicalType::Uuid
| CanonicalType::Jsonb
| CanonicalType::Timestamp
| CanonicalType::TimestampNtz
| CanonicalType::Date
| CanonicalType::Time
| CanonicalType::Timetz
| CanonicalType::Bytes
| CanonicalType::Serial
| CanonicalType::BigSerial
| CanonicalType::Array(_)
| CanonicalType::Custom(_) => TypeSupport::Native(ddl_type(t).leak()),
CanonicalType::Json => TypeSupport::Native("JSONB"),
CanonicalType::Asset => TypeSupport::Emulated("TEXT"),
CanonicalType::AssetArray => TypeSupport::Emulated("JSONB"),
}
}
pub fn type_category(t: &CanonicalType) -> TypeCategory {
super::types::type_category(t)
}
pub fn type_category_from_cast(cast: &str) -> TypeCategory {
super::types::type_category_from_cast(cast)
}
impl Dialect for PostgresDialect {
fn name(&self) -> &'static str {
"postgres"
}
fn ddl_type(&self, t: &CanonicalType) -> String {
ddl_type(t)
}
fn cast_name(&self, t: &CanonicalType) -> Option<String> {
cast_name(t)
}
fn type_category(&self, t: &CanonicalType) -> TypeCategory {
type_category(t)
}
fn type_support(&self, t: &CanonicalType) -> TypeSupport {
type_support(t)
}
fn quote_ident(&self, s: &str) -> String {
format!("\"{}\"", s.replace('"', "\"\""))
}
fn placeholder(&self, n: usize) -> String {
format!("${}", n)
}
fn cast_expr(&self, placeholder: &str, cast: &str) -> String {
format!("{}::{}", placeholder, cast)
}
fn now_fn(&self) -> &'static str {
"NOW()"
}
fn uuid_default_expr(&self) -> &'static str {
"gen_random_uuid()"
}
fn returning_clause(&self, cols: &str) -> String {
format!("RETURNING {}", cols)
}
fn upsert_conflict(&self, conflict_cols: &[&str], set_pairs: &str) -> String {
let cols = conflict_cols
.iter()
.map(|c| format!("\"{}\"", c.replace('"', "\"\"")))
.collect::<Vec<_>>()
.join(", ");
format!("ON CONFLICT ({}) DO UPDATE SET {}", cols, set_pairs)
}
fn to_one_subquery(&self, col_exprs: &[String], from_clause: &str) -> String {
let cols = col_exprs.join(", ");
format!(
"(SELECT row_to_json(sub) FROM (SELECT {} FROM {}) sub)",
cols, from_clause
)
}
fn to_many_subquery(&self, col_exprs: &[String], from_clause: &str) -> String {
let cols = col_exprs.join(", ");
format!(
"(SELECT COALESCE(json_agg(row_to_json(sub)), '[]'::json) FROM (SELECT {} FROM {}) sub)",
cols, from_clause
)
}
fn json_extract_text(&self, col: &str, key: &str) -> String {
format!("({} ->> '{}')", col, key.replace('\'', "''"))
}
fn json_extract_typed(&self, col: &str, key: &str, t: &CanonicalType) -> String {
let base = self.json_extract_text(col, key);
match cast_name(t) {
Some(cast) => format!("{}::{}", base, cast),
None => base,
}
}
fn case_insensitive_like(&self, col: &str, placeholder: &str) -> String {
format!("{} ILIKE {}", col, placeholder)
}
fn sys_json_type(&self) -> &'static str {
"JSONB"
}
fn sys_timestamp_type(&self) -> &'static str {
"TIMESTAMPTZ"
}
fn sys_bigserial_type(&self) -> &'static str {
"BIGSERIAL"
}
fn sys_bytes_type(&self) -> &'static str {
"BYTEA"
}
fn audit_timestamp_type(&self) -> &'static str {
"TIMESTAMPTZ"
}
fn supports_rls(&self) -> bool {
true
}
fn supports_named_enum_types(&self) -> bool {
true
}
fn supports_index_include(&self) -> bool {
true
}
fn set_tenant_session_sql(&self, tenant_id: &str) -> Option<String> {
Some(format!(
"SET LOCAL app.tenant_id = '{}'",
tenant_id.replace('\'', "''")
))
}
fn set_read_only_sql(&self) -> Option<String> {
Some("SET TRANSACTION READ ONLY".to_string())
}
fn set_statement_timeout_sql(&self, ms: u64) -> Option<String> {
Some(format!("SET LOCAL statement_timeout = {}", ms))
}
fn set_role_sql(&self, role: &str) -> Option<String> {
Some(format!("SET LOCAL ROLE {}", self.quote_ident(role)))
}
fn supports_add_column_if_not_exists(&self) -> bool {
true
}
fn is_duplicate_object_code(&self, code: &str) -> bool {
matches!(
code,
"42701" | "42P07" | "42710" | "42P06" | "42712"
)
}
fn introspect_columns_sql(&self, schema: &str) -> String {
format!(
"SELECT c.relname::text, a.attname::text, \
format_type(a.atttypid, a.atttypmod)::text, \
CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END, \
CASE WHEN a.atthasdef THEN 'YES' ELSE 'NO' END \
FROM pg_attribute a \
JOIN pg_class c ON c.oid = a.attrelid \
JOIN pg_namespace n ON n.oid = c.relnamespace \
WHERE n.nspname = '{}' AND c.relkind IN ('r', 'p') \
AND a.attnum > 0 AND NOT a.attisdropped",
escape_literal(schema)
)
}
fn introspect_indexes_sql(&self, schema: &str) -> Option<String> {
Some(format!(
"SELECT c.relname::text FROM pg_class c \
JOIN pg_namespace n ON n.oid = c.relnamespace \
WHERE n.nspname = '{}' AND c.relkind IN ('i', 'I')",
escape_literal(schema)
))
}
fn introspect_constraints_sql(&self, schema: &str) -> Option<String> {
Some(format!(
"SELECT t.relname::text, con.conname::text FROM pg_constraint con \
JOIN pg_class t ON t.oid = con.conrelid \
JOIN pg_namespace n ON n.oid = t.relnamespace \
WHERE n.nspname = '{}'",
escape_literal(schema)
))
}
}
#[cfg(test)]
mod cast_name_tests {
use super::cast_name;
use crate::db::CanonicalType;
#[test]
fn serial_types_cast_as_their_integer_width() {
assert_eq!(
cast_name(&CanonicalType::Serial).as_deref(),
Some("integer")
);
assert_eq!(
cast_name(&CanonicalType::BigSerial).as_deref(),
Some("bigint")
);
assert_eq!(cast_name(&CanonicalType::Int).as_deref(), Some("integer"));
assert_eq!(cast_name(&CanonicalType::BigInt).as_deref(), Some("bigint"));
}
#[test]
fn text_types_remain_uncast() {
assert_eq!(cast_name(&CanonicalType::Text), None);
}
}