architect_sdk/db/dialect.rs
1//! The `Dialect` trait — every database backend implements this.
2//!
3//! All methods are on `&self` so the same dialect object can be stored in `Arc<dyn Dialect>`.
4//! The trait is object-safe: no generics, no associated types.
5
6use super::types::{CanonicalType, TypeCategory, TypeSupport};
7
8/// A database dialect encapsulates all SQL syntax and type-mapping differences between
9/// database engines. The SDK calls dialect methods instead of hardcoding Postgres strings,
10/// so adding a new database is a matter of implementing this trait.
11pub trait Dialect: Send + Sync + 'static {
12 /// Short name for log messages and errors (e.g. "postgres", "mysql", "sqlite").
13 fn name(&self) -> &'static str;
14
15 // ── Type system ───────────────────────────────────────────────────────────
16
17 /// DDL type string for CREATE TABLE (e.g. "TIMESTAMPTZ", "DATETIME", "TEXT").
18 fn ddl_type(&self, t: &CanonicalType) -> String;
19
20 /// Type name used in parameter cast expressions, or `None` when no cast is needed.
21 /// Postgres: becomes `$n::cast`. MySQL/SQLite: cast is omitted (binding handles type).
22 fn cast_name(&self, t: &CanonicalType) -> Option<String>;
23
24 /// Broad category for RSQL operator validation.
25 fn type_category(&self, t: &CanonicalType) -> TypeCategory;
26
27 /// How well this dialect supports the canonical type.
28 fn type_support(&self, t: &CanonicalType) -> TypeSupport;
29
30 // ── Identifier quoting ────────────────────────────────────────────────────
31
32 /// Wrap an identifier in dialect-specific delimiters.
33 /// Postgres/SQLite: double-quotes. MySQL: backticks.
34 fn quote_ident(&self, s: &str) -> String;
35
36 // ── Parameter placeholders ────────────────────────────────────────────────
37
38 /// Positional placeholder for the n-th parameter (1-based).
39 /// Postgres: `$1`. MySQL/SQLite: `?`.
40 fn placeholder(&self, n: usize) -> String;
41
42 /// Wrap a placeholder with a type cast where required.
43 /// Postgres: `$1::uuid`. MySQL/SQLite: placeholder returned unchanged.
44 fn cast_expr(&self, placeholder: &str, cast: &str) -> String;
45
46 // ── SQL functions ─────────────────────────────────────────────────────────
47
48 /// Current-timestamp function name/expression.
49 fn now_fn(&self) -> &'static str;
50
51 /// Expression that generates a random UUID as a column DEFAULT.
52 fn uuid_default_expr(&self) -> &'static str;
53
54 // ── DML clauses ───────────────────────────────────────────────────────────
55
56 /// RETURNING clause appended to INSERT/UPDATE/DELETE, or empty string when unsupported.
57 fn returning_clause(&self, cols: &str) -> String;
58
59 /// Upsert conflict suffix.
60 /// `conflict_cols`: columns that identify the conflict.
61 /// `set_pairs`: pre-built "col = value" pairs for the update branch.
62 fn upsert_conflict(&self, conflict_cols: &[&str], set_pairs: &str) -> String;
63
64 // ── JSON aggregation (related-entity includes) ────────────────────────────
65
66 /// Build a scalar subquery returning a single JSON object for a to-one include.
67 /// `col_exprs`: already-quoted column expressions.
68 /// `from_clause`: `"schema"."table" WHERE ...` fragment.
69 fn to_one_subquery(&self, col_exprs: &[String], from_clause: &str) -> String;
70
71 /// Build a scalar subquery returning a JSON array for a to-many include.
72 fn to_many_subquery(&self, col_exprs: &[String], from_clause: &str) -> String;
73
74 // ── JSON path extraction (extensible fields) ───────────────────────────────────
75
76 /// Extract a top-level key from a JSON/JSONB column as text.
77 /// `col` is an already-quoted column expression; `key` is the raw JSON key (escaped here).
78 /// Postgres: `(col ->> 'key')`. MySQL/SQLite: `col->>'$.key'`.
79 fn json_extract_text(&self, col: &str, key: &str) -> String;
80
81 /// Extract a JSON key and cast it to `t` so comparisons and ORDER BY are type-correct.
82 /// For text-like types this is equivalent to [`Dialect::json_extract_text`].
83 fn json_extract_typed(&self, col: &str, key: &str, t: &CanonicalType) -> String;
84
85 /// Case-insensitive LIKE comparison fragment: `col <ci-like> placeholder`.
86 /// Postgres: `col ILIKE ph`. MySQL: `LOWER(col) LIKE LOWER(ph)`. SQLite: `col LIKE ph`
87 /// (SQLite LIKE is case-insensitive for ASCII by default).
88 fn case_insensitive_like(&self, col: &str, placeholder: &str) -> String;
89
90 // ── System-table DDL helpers ──────────────────────────────────────────────
91
92 /// DDL fragment for a JSON/JSONB payload column (e.g. "JSONB", "JSON", "TEXT").
93 fn sys_json_type(&self) -> &'static str;
94
95 /// Timestamp type name (without NOT NULL / DEFAULT).
96 fn sys_timestamp_type(&self) -> &'static str;
97
98 /// NOT NULL timestamp column with a now() default — convenience built from above.
99 fn sys_timestamp_default(&self) -> String {
100 format!(
101 "{} NOT NULL DEFAULT {}",
102 self.sys_timestamp_type(),
103 self.now_fn()
104 )
105 }
106
107 /// Auto-incrementing large integer for surrogate PKs.
108 /// e.g. "BIGSERIAL", "BIGINT AUTO_INCREMENT", "INTEGER".
109 fn sys_bigserial_type(&self) -> &'static str;
110
111 /// DDL type for a raw binary payload column (e.g. "BYTEA", "BLOB").
112 fn sys_bytes_type(&self) -> &'static str;
113
114 /// Timestamp type used in audit table columns (no DEFAULT — values supplied explicitly).
115 fn audit_timestamp_type(&self) -> &'static str;
116
117 // ── Multi-tenancy ─────────────────────────────────────────────────────────
118
119 /// Whether this dialect supports `CREATE SCHEMA` DDL.
120 /// Postgres: true. MySQL: false (uses databases). SQLite: false (no user-defined schemas).
121 fn supports_schemas(&self) -> bool {
122 true
123 }
124
125 /// DDL fragment for a column that holds a timestamp defaulting to N hours from now.
126 /// Returns `None` when the dialect has no constant-expression equivalent (SQLite).
127 /// Callers should make the column nullable and omit the DEFAULT when `None` is returned.
128 fn default_now_plus_hours(&self, hours: u32) -> Option<String> {
129 Some(format!("NOW() + INTERVAL '{} hours'", hours))
130 }
131
132 /// Whether this dialect natively supports row-level security (CREATE POLICY etc.).
133 fn supports_rls(&self) -> bool;
134
135 /// Whether this dialect supports named enum types (CREATE TYPE … AS ENUM).
136 fn supports_named_enum_types(&self) -> bool;
137
138 /// Whether this dialect supports INCLUDE columns on indexes (Postgres 11+).
139 fn supports_index_include(&self) -> bool;
140
141 /// SQL statement that sets a session-local tenant identifier before a query.
142 /// Returns `None` when the dialect has no such mechanism.
143 fn set_tenant_session_sql(&self, tenant_id: &str) -> Option<String>;
144}