• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joaoh82 / rust_sqlite / 25278812660

03 May 2026 12:11PM UTC coverage: 56.503% (-0.4%) from 56.877%
25278812660

push

github

web-flow
cleanup(engine): make process_command stdout-clean (drop REPL-only prints) (#76)

The engine's `process_command` had three direct stdout writes that
made sense in a REPL but corrupted any other channel an embedder
might be using stdout for:

  src/sql/mod.rs:150 — let _ = table.print_table_schema();   (CREATE)
  src/sql/mod.rs:208 — db_table.print_table_data();          (INSERT)
  src/sql/mod.rs:224 — print!("{rendered}");                 (SELECT)

Plus a leftover debug print in src/sql/parser/create.rs:190 dumping
unhandled `sqlparser::ast::TableConstraint` debug reprs to stdout.

This is the issue PR #73 papered over with the dup2(2,1) dance in
sqlrite-mcp/src/stdio_redirect.rs. That fix is still in place
(belt-and-suspenders against future regressions), but the engine now
behaves correctly without it — the SDKs (Python, Node, Go, WASM,
FFI), the Tauri desktop, and any future embedder all benefit.

## How

Refactored process_command to return a richer struct, with a
backwards-compat wrapper:

  pub struct CommandOutput {
      pub status:   String,           // "INSERT Statement executed.", etc.
      pub rendered: Option<String>,   // SELECT prettytable; None otherwise
  }

  pub fn process_command(query, db) -> Result<String>
  // backwards-compat: returns just .status

  pub fn process_command_with_render(query, db) -> Result<CommandOutput>
  // new: rich return, never writes to stdout

The REPL (`src/main.rs`) calls `_with_render` and prints rendered
above status — same UX as before. The .ask meta-command does the
same and concatenates the two pieces into the single String it
bubbles up to the REPL's outer dispatch loop.

The CREATE-TABLE schema dump and INSERT row dump aren't preserved —
they were small UX niceties that interactive REPL users rarely need
(the schema you just CREATEd, the row you just INSERTed). The INSERT
case was actively bad UX on any non-trivial table — it dumped the
entire table after every insert. Both gone.

`sqlrite::pro... (continued)

20 of 30 new or added lines in 4 files covered. (66.67%)

38 existing lines in 3 files now uncovered.

5465 of 9672 relevant lines covered (56.5%)

1.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.0
/src/sql/parser/create.rs
1
use sqlparser::ast::{ColumnOption, CreateTable, DataType, ObjectName, ObjectNamePart, Statement};
2

3
use crate::error::{Result, SQLRiteError};
4

5
/// True when an `ObjectName` resolves to a single identifier `VECTOR`
6
/// (case-insensitive). Phase 7a adds the `VECTOR(N)` column type as a
7
/// sqlparser `DataType::Custom` — the engine recognizes it via this
8
/// helper so the regular DataType match arm above stays uncluttered.
9
fn is_vector_type(name: &ObjectName) -> bool {
1✔
10
    name.0.len() == 1
1✔
11
        && match &name.0[0] {
1✔
12
            ObjectNamePart::Identifier(ident) => ident.value.eq_ignore_ascii_case("VECTOR"),
1✔
13
            // Function-form ObjectNamePart shouldn't appear in a CREATE TABLE
14
            // column type position. If it ever does, treat it as not-a-vector
15
            // and the outer match falls through to the "Invalid" arm.
16
            _ => false,
×
17
        }
18
}
19

20
/// Parses the dimension out of the `Custom` args for `VECTOR(N)`.
21
/// `args` is the `Vec<String>` sqlparser hands back for parenthesized
22
/// type arguments — for `VECTOR(384)` that's `["384"]`. Validates that
23
/// exactly one positive-integer argument was supplied.
24
fn parse_vector_dim(args: &[String]) -> std::result::Result<usize, String> {
1✔
25
    match args {
26
        [] => Err("VECTOR requires a dimension, e.g. `VECTOR(384)`".to_string()),
2✔
27
        [single] => {
2✔
28
            let trimmed = single.trim();
1✔
29
            match trimmed.parse::<usize>() {
1✔
30
                Ok(d) if d > 0 => Ok(d),
2✔
31
                Ok(_) => Err(format!("VECTOR dimension must be ≥ 1 (got `{trimmed}`)")),
1✔
32
                Err(_) => Err(format!(
×
33
                    "VECTOR dimension must be a positive integer (got `{trimmed}`)"
34
                )),
35
            }
36
        }
37
        many => Err(format!(
×
38
            "VECTOR takes exactly one dimension argument (got {})",
39
            many.len()
×
40
        )),
41
    }
42
}
43

44
/// The schema for each SQL column in every table is represented by
45
/// the following structure after parsed and tokenized
46
#[derive(PartialEq, Debug)]
47
pub struct ParsedColumn {
48
    /// Name of the column
49
    pub name: String,
50
    /// Datatype of the column in String format
51
    pub datatype: String,
52
    /// Value representing if column is PRIMARY KEY
53
    pub is_pk: bool,
54
    /// Value representing if column was declared with the NOT NULL Constraint
55
    pub not_null: bool,
56
    /// Value representing if column was declared with the UNIQUE Constraint
57
    pub is_unique: bool,
58
}
59

60
/// The following structure represents a CREATE TABLE query already parsed
61
/// and broken down into name and a Vector of `ParsedColumn` metadata
62
///
63
#[derive(Debug)]
64
pub struct CreateQuery {
65
    /// name of table after parking and tokenizing of query
66
    pub table_name: String,
67
    /// Vector of `ParsedColumn` type with column metadata information
68
    pub columns: Vec<ParsedColumn>,
69
}
70

71
impl CreateQuery {
72
    pub fn new(statement: &Statement) -> Result<CreateQuery> {
3✔
73
        match statement {
2✔
74
            // Confirming the Statement is sqlparser::ast:Statement::CreateTable
75
            Statement::CreateTable(CreateTable {
×
76
                name,
2✔
77
                columns,
2✔
78
                constraints,
2✔
79
                ..
×
80
            }) => {
×
81
                let table_name = name;
2✔
82
                let mut parsed_columns: Vec<ParsedColumn> = vec![];
2✔
83

84
                // Iterating over the columns returned form the Parser::parse:sql
85
                // in the mod sql
86
                for col in columns {
6✔
87
                    let name = col.name.to_string();
4✔
88

89
                    // Checks if columm already added to parsed_columns, if so, returns an error
90
                    if parsed_columns.iter().any(|col| col.name == name) {
8✔
91
                        return Err(SQLRiteError::Internal(format!(
×
92
                            "Duplicate column name: {}",
×
93
                            &name
×
94
                        )));
95
                    }
96

97
                    // Parsing each column for it data type
98
                    // For now only accepting basic data types
99
                    let datatype: String = match &col.data_type {
2✔
100
                        DataType::TinyInt(_)
×
101
                        | DataType::SmallInt(_)
×
102
                        | DataType::Int2(_)
×
103
                        | DataType::Int(_)
×
104
                        | DataType::Int4(_)
×
105
                        | DataType::Int8(_)
×
106
                        | DataType::Integer(_)
×
107
                        | DataType::BigInt(_) => "Integer".to_string(),
4✔
108
                        DataType::Boolean => "Bool".to_string(),
2✔
109
                        DataType::Text => "Text".to_string(),
4✔
110
                        DataType::Varchar(_bytes) => "Text".to_string(),
×
111
                        DataType::Real => "Real".to_string(),
2✔
112
                        DataType::Float(_precision) => "Real".to_string(),
×
113
                        DataType::Double(_) => "Real".to_string(),
×
114
                        DataType::Decimal(_) => "Real".to_string(),
×
115
                        // Phase 7e — `JSON` parses as a unit variant in
116
                        // sqlparser's DataType enum. JSONB is treated as
117
                        // an alias (matches PostgreSQL's permissive
118
                        // behaviour); both store as text under the hood.
119
                        DataType::JSON | DataType::JSONB => "Json".to_string(),
2✔
120
                        // Phase 7a — `VECTOR(N)` parses as Custom("VECTOR", ["N"]).
121
                        // sqlparser's SQLite dialect doesn't have a built-in
122
                        // Vector variant; Custom is what unrecognized type
123
                        // names + their parenthesized args fall through to.
124
                        DataType::Custom(name, args) if is_vector_type(name) => {
2✔
125
                            match parse_vector_dim(args) {
1✔
126
                                Ok(dim) => format!("vector({dim})"),
1✔
127
                                Err(e) => {
1✔
128
                                    return Err(SQLRiteError::General(format!(
2✔
129
                                        "Invalid VECTOR column '{}': {e}",
×
130
                                        col.name
×
131
                                    )));
132
                                }
133
                            }
134
                        }
135
                        other => {
1✔
136
                            eprintln!("not matched on custom type: {other:?}");
2✔
137
                            "Invalid".to_string()
1✔
138
                        }
139
                    };
140

141
                    // checking if column is PRIMARY KEY
142
                    let mut is_pk: bool = false;
2✔
143
                    // chekcing if column is UNIQUE
144
                    let mut is_unique: bool = false;
2✔
145
                    // chekcing if column is NULLABLE
146
                    let mut not_null: bool = false;
2✔
147
                    for column_option in &col.options {
4✔
148
                        match &column_option.option {
2✔
149
                            ColumnOption::PrimaryKey(_) => {
×
150
                                // For now, only Integer and Text types can be PRIMARY KEY and Unique
151
                                // Therefore Indexed.
152
                                if datatype != "Real" && datatype != "Bool" {
6✔
153
                                    // Checks if table being created already has a PRIMARY KEY, if so, returns an error
154
                                    if parsed_columns.iter().any(|col| col.is_pk) {
2✔
155
                                        return Err(SQLRiteError::Internal(format!(
×
156
                                            "Table '{}' has more than one primary key",
×
157
                                            &table_name
×
158
                                        )));
159
                                    }
160
                                    is_pk = true;
2✔
161
                                    is_unique = true;
2✔
162
                                    not_null = true;
2✔
163
                                }
164
                            }
165
                            ColumnOption::Unique(_) => {
×
166
                                // For now, only Integer and Text types can be UNIQUE
167
                                // Therefore Indexed.
168
                                if datatype != "Real" && datatype != "Bool" {
3✔
169
                                    is_unique = true;
1✔
170
                                }
171
                            }
172
                            ColumnOption::NotNull => {
2✔
173
                                not_null = true;
2✔
174
                            }
175
                            _ => (),
×
176
                        };
177
                    }
178

179
                    parsed_columns.push(ParsedColumn {
2✔
180
                        name,
2✔
181
                        datatype: datatype.to_string(),
2✔
182
                        is_pk,
2✔
183
                        not_null,
2✔
184
                        is_unique,
2✔
185
                    });
186
                }
187
                // TODO: handle constraints + default values + check
188
                // constraints + ON DELETE / ON UPDATE referential actions
189
                // properly. They're currently parsed by `sqlparser` and
190
                // dropped on the floor here. (Previously we `println!`-ed
191
                // them to stdout as a debug aid — removed in the
192
                // engine-stdout-pollution cleanup; flip to a `tracing`
193
                // span if we ever want them visible in dev builds.)
NEW
194
                let _ = constraints;
×
195
                Ok(CreateQuery {
2✔
196
                    table_name: table_name.to_string(),
2✔
197
                    columns: parsed_columns,
2✔
198
                })
199
            }
200

201
            _ => Err(SQLRiteError::Internal("Error parsing query".to_string())),
×
202
        }
203
    }
204
}
205

206
#[cfg(test)]
207
mod tests {
208
    use super::*;
209
    use crate::sql::*;
210

211
    #[test]
212
    fn create_table_validate_tablename_test() {
3✔
213
        let sql_input = String::from(
214
            "CREATE TABLE contacts (
215
            id INTEGER PRIMARY KEY,
216
            first_name TEXT NOT NULL,
217
            last_name TEXT NOT NULl,
218
            email TEXT NOT NULL UNIQUE
219
        );",
220
        );
221
        let expected_table_name = String::from("contacts");
1✔
222

223
        let dialect = SQLiteDialect {};
224
        let mut ast = Parser::parse_sql(&dialect, &sql_input).unwrap();
2✔
225

226
        assert!(ast.len() == 1, "ast has more then one Statement");
2✔
227

228
        let query = ast.pop().unwrap();
2✔
229

230
        // Initialy only implementing some basic SQL Statements
231
        if let Statement::CreateTable(_) = query {
1✔
232
            let result = CreateQuery::new(&query);
1✔
233
            match result {
1✔
234
                Ok(payload) => {
1✔
235
                    assert_eq!(payload.table_name, expected_table_name);
2✔
236
                }
237
                Err(_) => panic!("an error occured during parsing CREATE TABLE Statement"),
×
238
            }
239
        }
240
    }
241
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc