sochdb_query/sql/
compatibility.rs

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