Skip to main content

sochdb_query/sql/
compatibility.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! # SQL Compatibility Matrix
19//!
20//! This module defines SochDB's SQL dialect support and compatibility layer.
21//!
22//! ## Design Goals
23//!
24//! 1. **Portable Core**: SQL-92 compatible baseline that works across ecosystems
25//! 2. **Dialect Sugar**: Support common dialect variants (MySQL, PostgreSQL, SQLite)
26//! 3. **Single AST**: All dialects normalize to one canonical AST representation
27//! 4. **Extensible**: Add new dialects without forking parsers/executors
28//!
29//! ## SQL Feature Matrix
30//!
31//! ### Guaranteed (Core SQL)
32//!
33//! | Category | Statement | Status | Notes |
34//! |----------|-----------|--------|-------|
35//! | DML | SELECT | ✅ | With WHERE, ORDER BY, LIMIT, OFFSET |
36//! | DML | INSERT | ✅ | Single and multi-row |
37//! | DML | UPDATE | ✅ | With WHERE clause |
38//! | DML | DELETE | ✅ | With WHERE clause |
39//! | DDL | CREATE TABLE | ✅ | With column types and constraints |
40//! | DDL | DROP TABLE | ✅ | Basic form |
41//! | DDL | ALTER TABLE | 🔄 | ADD/DROP COLUMN |
42//! | DDL | CREATE INDEX | ✅ | Single and multi-column |
43//! | DDL | DROP INDEX | ✅ | Basic form |
44//! | Tx | BEGIN | ✅ | Start transaction |
45//! | Tx | COMMIT | ✅ | Commit transaction |
46//! | Tx | ROLLBACK | ✅ | Rollback transaction |
47//!
48//! ### Idempotent DDL
49//!
50//! | Statement | Status | Notes |
51//! |-----------|--------|-------|
52//! | CREATE TABLE IF NOT EXISTS | ✅ | No-op if exists |
53//! | DROP TABLE IF EXISTS | ✅ | No-op if not exists |
54//! | CREATE INDEX IF NOT EXISTS | ✅ | No-op if exists |
55//! | DROP INDEX IF EXISTS | ✅ | No-op if not exists |
56//!
57//! ### Conflict/Upsert Family
58//!
59//! All of these normalize to `InsertStmt { on_conflict: Some(OnConflict { .. }) }`
60//!
61//! | Dialect | Syntax | Canonical AST |
62//! |---------|--------|---------------|
63//! | PostgreSQL | `ON CONFLICT DO NOTHING` | `OnConflict { action: DoNothing }` |
64//! | PostgreSQL | `ON CONFLICT DO UPDATE SET ...` | `OnConflict { action: DoUpdate(...) }` |
65//! | MySQL | `INSERT IGNORE` | `OnConflict { action: DoNothing }` |
66//! | MySQL | `ON DUPLICATE KEY UPDATE` | `OnConflict { action: DoUpdate(...) }` |
67//! | SQLite | `INSERT OR IGNORE` | `OnConflict { action: DoNothing }` |
68//! | SQLite | `INSERT OR REPLACE` | `OnConflict { action: DoReplace }` |
69//!
70//! ### Out of Scope (Explicit Limitations)
71//!
72//! | Feature | Status | Reason |
73//! |---------|--------|--------|
74//! | Multi-table JOINs | ❌ | Complexity; single-table focus for v1 |
75//! | Subqueries in WHERE | ❌ | Planning complexity |
76//! | Window functions | ❌ | Future enhancement |
77//! | CTEs (WITH clause) | ❌ | Future enhancement |
78//! | Stored procedures | ❌ | Out of scope |
79//!
80//! ## Dialect Detection
81//!
82//! SochDB auto-detects dialect from syntax:
83//! - `INSERT IGNORE` → MySQL mode
84//! - `INSERT OR IGNORE` → SQLite mode
85//! - `ON CONFLICT` → PostgreSQL mode
86//!
87//! All normalize to the same internal representation.
88
89use std::fmt;
90
91/// SQL Dialect for parsing/normalization
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub enum SqlDialect {
94    /// Standard SQL-92 compatible (default)
95    #[default]
96    Standard,
97    /// PostgreSQL dialect
98    PostgreSQL,
99    /// MySQL dialect
100    MySQL,
101    /// SQLite dialect
102    SQLite,
103}
104
105impl SqlDialect {
106    /// Detect dialect from SQL text
107    pub fn detect(sql: &str) -> Self {
108        let upper = sql.to_uppercase();
109
110        // MySQL: INSERT IGNORE
111        if upper.contains("INSERT IGNORE") || upper.contains("ON DUPLICATE KEY") {
112            return SqlDialect::MySQL;
113        }
114
115        // SQLite: INSERT OR IGNORE/REPLACE/ABORT
116        if upper.contains("INSERT OR IGNORE")
117            || upper.contains("INSERT OR REPLACE")
118            || upper.contains("INSERT OR ABORT")
119        {
120            return SqlDialect::SQLite;
121        }
122
123        // PostgreSQL: ON CONFLICT
124        if upper.contains("ON CONFLICT") {
125            return SqlDialect::PostgreSQL;
126        }
127
128        SqlDialect::Standard
129    }
130}
131
132impl fmt::Display for SqlDialect {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            SqlDialect::Standard => write!(f, "Standard SQL"),
136            SqlDialect::PostgreSQL => write!(f, "PostgreSQL"),
137            SqlDialect::MySQL => write!(f, "MySQL"),
138            SqlDialect::SQLite => write!(f, "SQLite"),
139        }
140    }
141}
142
143/// SQL Feature support level
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum FeatureSupport {
146    /// Fully supported
147    Full,
148    /// Partially supported with limitations
149    Partial,
150    /// Planned for future release
151    Planned,
152    /// Not supported and not planned
153    NotSupported,
154}
155
156impl fmt::Display for FeatureSupport {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            FeatureSupport::Full => write!(f, "✅ Full"),
160            FeatureSupport::Partial => write!(f, "🔄 Partial"),
161            FeatureSupport::Planned => write!(f, "📋 Planned"),
162            FeatureSupport::NotSupported => write!(f, "❌ Not Supported"),
163        }
164    }
165}
166
167/// SQL Feature categories
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub enum SqlFeature {
170    // DML
171    Select,
172    Insert,
173    Update,
174    Delete,
175
176    // DDL
177    CreateTable,
178    DropTable,
179    AlterTable,
180    CreateIndex,
181    DropIndex,
182
183    // Idempotent DDL
184    CreateTableIfNotExists,
185    DropTableIfExists,
186    CreateIndexIfNotExists,
187    DropIndexIfExists,
188
189    // Conflict/Upsert
190    OnConflictDoNothing,
191    OnConflictDoUpdate,
192    InsertIgnore,
193    InsertOrIgnore,
194    InsertOrReplace,
195    OnDuplicateKeyUpdate,
196
197    // Transactions
198    Begin,
199    Commit,
200    Rollback,
201    Savepoint,
202
203    // Query features
204    Where,
205    OrderBy,
206    Limit,
207    Offset,
208    GroupBy,
209    Having,
210    Distinct,
211
212    // Joins (limited)
213    InnerJoin,
214    LeftJoin,
215    RightJoin,
216    CrossJoin,
217
218    // Subqueries
219    SubqueryInFrom,
220    SubqueryInWhere,
221    SubqueryInSelect,
222
223    // Set operations
224    Union,
225    Intersect,
226    Except,
227
228    // Expressions
229    ParameterizedQueries,
230    CaseWhen,
231    Cast,
232    NullHandling,
233    InList,
234    Between,
235    Like,
236
237    // SochDB extensions
238    VectorSearch,
239    EmbeddingType,
240    ContextWindow,
241}
242
243/// Get feature support level
244pub fn get_feature_support(feature: SqlFeature) -> FeatureSupport {
245    use SqlFeature::*;
246
247    match feature {
248        // Fully supported
249        Select | Insert | Update | Delete => FeatureSupport::Full,
250        CreateTable | DropTable | CreateIndex | DropIndex => FeatureSupport::Full,
251        CreateTableIfNotExists | DropTableIfExists => FeatureSupport::Full,
252        CreateIndexIfNotExists | DropIndexIfExists => FeatureSupport::Full,
253        Begin | Commit | Rollback => FeatureSupport::Full,
254        Where | OrderBy | Limit | Offset | Distinct => FeatureSupport::Full,
255        ParameterizedQueries | NullHandling | InList | Like => FeatureSupport::Full,
256        OnConflictDoNothing | InsertIgnore | InsertOrIgnore => FeatureSupport::Full,
257        VectorSearch | EmbeddingType => FeatureSupport::Full,
258
259        // Partially supported
260        AlterTable => FeatureSupport::Partial, // ADD/DROP COLUMN only
261        GroupBy | Having => FeatureSupport::Partial, // Basic support
262        InnerJoin => FeatureSupport::Partial, // Two-table only
263        OnConflictDoUpdate | InsertOrReplace | OnDuplicateKeyUpdate => FeatureSupport::Partial,
264        CaseWhen | Cast | Between => FeatureSupport::Partial,
265        Union => FeatureSupport::Partial,
266        SubqueryInFrom => FeatureSupport::Partial,
267        Savepoint => FeatureSupport::Partial,
268        ContextWindow => FeatureSupport::Partial,
269
270        // Planned
271        LeftJoin | RightJoin | CrossJoin => FeatureSupport::Planned,
272        SubqueryInWhere | SubqueryInSelect => FeatureSupport::Planned,
273        Intersect | Except => FeatureSupport::Planned,
274    }
275}
276
277/// Compatibility matrix for different SQL dialects
278pub struct CompatibilityMatrix;
279
280impl CompatibilityMatrix {
281    /// Check if a feature is supported
282    pub fn is_supported(feature: SqlFeature) -> bool {
283        matches!(
284            get_feature_support(feature),
285            FeatureSupport::Full | FeatureSupport::Partial
286        )
287    }
288
289    /// Get all fully supported features
290    pub fn fully_supported() -> Vec<SqlFeature> {
291        use SqlFeature::*;
292        vec![
293            Select,
294            Insert,
295            Update,
296            Delete,
297            CreateTable,
298            DropTable,
299            CreateIndex,
300            DropIndex,
301            CreateTableIfNotExists,
302            DropTableIfExists,
303            CreateIndexIfNotExists,
304            DropIndexIfExists,
305            Begin,
306            Commit,
307            Rollback,
308            Where,
309            OrderBy,
310            Limit,
311            Offset,
312            Distinct,
313            ParameterizedQueries,
314            NullHandling,
315            InList,
316            Like,
317            OnConflictDoNothing,
318            InsertIgnore,
319            InsertOrIgnore,
320            VectorSearch,
321            EmbeddingType,
322        ]
323    }
324
325    /// Print the compatibility matrix as a formatted table
326    pub fn print_matrix() -> String {
327        let mut output = String::new();
328        output.push_str("# SochDB SQL Compatibility Matrix\n\n");
329
330        output.push_str("## Core DML\n\n");
331        output.push_str("| Feature | Support |\n");
332        output.push_str("|---------|--------|\n");
333        for feature in &[
334            SqlFeature::Select,
335            SqlFeature::Insert,
336            SqlFeature::Update,
337            SqlFeature::Delete,
338        ] {
339            output.push_str(&format!(
340                "| {:?} | {} |\n",
341                feature,
342                get_feature_support(*feature)
343            ));
344        }
345
346        output.push_str("\n## DDL\n\n");
347        output.push_str("| Feature | Support |\n");
348        output.push_str("|---------|--------|\n");
349        for feature in &[
350            SqlFeature::CreateTable,
351            SqlFeature::DropTable,
352            SqlFeature::AlterTable,
353            SqlFeature::CreateIndex,
354            SqlFeature::DropIndex,
355            SqlFeature::CreateTableIfNotExists,
356            SqlFeature::DropTableIfExists,
357        ] {
358            output.push_str(&format!(
359                "| {:?} | {} |\n",
360                feature,
361                get_feature_support(*feature)
362            ));
363        }
364
365        output.push_str("\n## Conflict/Upsert\n\n");
366        output.push_str("| Feature | Support |\n");
367        output.push_str("|---------|--------|\n");
368        for feature in &[
369            SqlFeature::OnConflictDoNothing,
370            SqlFeature::OnConflictDoUpdate,
371            SqlFeature::InsertIgnore,
372            SqlFeature::InsertOrIgnore,
373            SqlFeature::InsertOrReplace,
374            SqlFeature::OnDuplicateKeyUpdate,
375        ] {
376            output.push_str(&format!(
377                "| {:?} | {} |\n",
378                feature,
379                get_feature_support(*feature)
380            ));
381        }
382
383        output
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_dialect_detection() {
393        assert_eq!(SqlDialect::detect("SELECT * FROM users"), SqlDialect::Standard);
394        assert_eq!(
395            SqlDialect::detect("INSERT IGNORE INTO users VALUES (1)"),
396            SqlDialect::MySQL
397        );
398        assert_eq!(
399            SqlDialect::detect("INSERT OR IGNORE INTO users VALUES (1)"),
400            SqlDialect::SQLite
401        );
402        assert_eq!(
403            SqlDialect::detect("INSERT INTO users VALUES (1) ON CONFLICT DO NOTHING"),
404            SqlDialect::PostgreSQL
405        );
406    }
407
408    #[test]
409    fn test_feature_support() {
410        assert_eq!(get_feature_support(SqlFeature::Select), FeatureSupport::Full);
411        assert_eq!(
412            get_feature_support(SqlFeature::AlterTable),
413            FeatureSupport::Partial
414        );
415        assert_eq!(
416            get_feature_support(SqlFeature::LeftJoin),
417            FeatureSupport::Planned
418        );
419    }
420
421    #[test]
422    fn test_compatibility_matrix() {
423        assert!(CompatibilityMatrix::is_supported(SqlFeature::Select));
424        assert!(CompatibilityMatrix::is_supported(SqlFeature::AlterTable));
425        assert!(!CompatibilityMatrix::is_supported(SqlFeature::LeftJoin));
426    }
427}