├── go.work ├── .gitignore ├── .golangci.yml ├── examples └── gormapp │ ├── go.mod │ ├── go.sum │ ├── models.go │ └── README.md ├── cmd ├── stoolap-pgserver │ ├── go.mod │ ├── main.go │ └── go.sum └── stoolap │ ├── go.mod │ └── go.sum ├── internal ├── storage │ ├── mvcc │ │ ├── sync_windows.go │ │ ├── sync_unix.go │ │ ├── file_lock_unix.go │ │ ├── file_lock_windows.go │ │ ├── file_lock.go │ │ ├── mvcc.go │ │ └── columnar_index_multi_simple_test.go │ ├── factory.go │ ├── bitmap │ │ ├── like_implementation.go │ │ └── like_pattern.go │ ├── expression │ │ └── not_expression.go │ ├── binser │ │ └── json_encoder.go │ └── compression │ │ └── bitpack.go ├── common │ ├── version.go │ ├── types.go │ └── test_helpers.go ├── parser │ ├── sql_types.go │ ├── parse.go │ ├── update_ast_tests.go │ ├── ast_hash.go │ ├── cast_parser_test.go │ ├── cast_parser.go │ ├── errors_test.go │ ├── view_parser.go │ ├── errors_suggestions_test.go │ └── funcregistry │ │ └── validator.go ├── sql │ ├── executor │ │ ├── params.go │ │ ├── temporal.go │ │ ├── columnar_aggregate.go │ │ └── qualified_result.go │ └── executor.go ├── functions │ ├── scalar │ │ ├── now.go │ │ ├── version.go │ │ ├── concat.go │ │ ├── length.go │ │ ├── lower.go │ │ ├── upper.go │ │ ├── abs.go │ │ ├── floor.go │ │ ├── ceiling.go │ │ ├── round.go │ │ └── utils.go │ ├── window │ │ └── row_number.go │ └── aggregate │ │ ├── utils.go │ │ ├── compare_test.go │ │ └── count.go └── fastmap │ └── simple_int64_benchmark_test.go ├── go.mod ├── test ├── merge_parser_test.go ├── memory_test.go ├── simple_distinct_test.go ├── cte_expression_alias_test.go ├── multi_statement_test.go ├── date_time_simple_test.go ├── hash_optimization_simple_test.go ├── simple_time_test.go ├── parameter_binding_benchmark_test.go ├── update_param_test.go ├── cte_exists_totals_test.go ├── cast_fix_test.go ├── json_test.go ├── string_quotes_test.go ├── parser_drop_index_test.go ├── cte_simple_alias_test.go ├── mvcc_simple_sum_test.go ├── as_of_parser_test.go ├── cte_registry_test.go ├── is_null_simple_test.go ├── column_alias_select_test.go ├── sql_literals_simple_test.go ├── cast_direct_test.go ├── update_parentheses_test.go ├── cast_column_alias_test.go ├── order_by_parser_test.go ├── date_format_test.go ├── update_select_comparison_test.go ├── stress_lost_update_test.go └── cast_evaluator_test.go ├── .github └── workflows │ └── claude.yml ├── codecov.yml └── go.work.sum /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.3 2 | 3 | use ( 4 | . 5 | ./cmd/stoolap 6 | ./cmd/stoolap-pgserver 7 | ./examples/gormapp 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/var 2 | var 3 | works 4 | .claude/settings.local.json 5 | CLAUDE.md 6 | TODO.md 7 | cmd/stoolap/stoolap 8 | cmd/stoolap-pgserver/stoolap-pgserver 9 | stoolap.wiki/ 10 | scripts/ 11 | coverage.txt 12 | *.out 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # golangci-lint configuration 2 | # https://golangci-lint.run/usage/configuration/ 3 | version: "2" 4 | 5 | run: 6 | timeout: 5m 7 | 8 | linters: 9 | disable: 10 | - errcheck 11 | - staticcheck 12 | - goconst 13 | - gosec 14 | enable: 15 | - govet 16 | - ineffassign 17 | - unused 18 | - misspell 19 | - unconvert 20 | -------------------------------------------------------------------------------- /examples/gormapp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stoolap/stoolap-go/examples/gormapp 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/stoolap/stoolap-go v0.1.3 7 | gorm.io/driver/mysql v1.5.4 8 | gorm.io/gorm v1.25.7 9 | ) 10 | 11 | require ( 12 | github.com/go-sql-driver/mysql v1.7.0 // indirect 13 | github.com/jinzhu/inflection v1.0.0 // indirect 14 | github.com/jinzhu/now v1.1.5 // indirect 15 | ) 16 | 17 | replace github.com/stoolap/stoolap-go => ../.. 18 | -------------------------------------------------------------------------------- /cmd/stoolap-pgserver/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stoolap/stoolap-go/cmd/stoolap-pgserver 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/jackc/pgx/v5 v5.7.5 7 | github.com/spf13/cobra v1.9.1 8 | github.com/stoolap/stoolap-go v0.1.3 9 | ) 10 | 11 | require ( 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/spf13/pflag v1.0.6 // indirect 14 | github.com/stretchr/testify v1.10.0 // indirect 15 | ) 16 | 17 | replace github.com/stoolap/stoolap-go => ../.. 18 | -------------------------------------------------------------------------------- /cmd/stoolap/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stoolap/stoolap-go/cmd/stoolap 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/chzyer/readline v1.5.1 7 | github.com/jedib0t/go-pretty/v6 v6.6.7 8 | github.com/spf13/cobra v1.9.1 9 | github.com/stoolap/stoolap-go v0.1.3 10 | ) 11 | 12 | require ( 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | github.com/mattn/go-runewidth v0.0.16 // indirect 15 | github.com/rivo/uniseg v0.4.7 // indirect 16 | github.com/spf13/pflag v1.0.6 // indirect 17 | golang.org/x/sys v0.30.0 // indirect 18 | golang.org/x/text v0.24.0 // indirect 19 | ) 20 | 21 | replace github.com/stoolap/stoolap-go => ../.. 22 | -------------------------------------------------------------------------------- /internal/storage/mvcc/sync_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | Copyright 2025 Stoolap Contributors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package mvcc 19 | 20 | import ( 21 | "os" 22 | ) 23 | 24 | // OptimizedSync uses file.Sync() on Windows 25 | func OptimizedSync(file *os.File) error { 26 | return file.Sync() 27 | } 28 | -------------------------------------------------------------------------------- /internal/storage/mvcc/sync_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | Copyright 2025 Stoolap Contributors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package mvcc 19 | 20 | import ( 21 | "os" 22 | "syscall" 23 | ) 24 | 25 | // OptimizedSync uses standard sync on Unix platforms 26 | func OptimizedSync(file *os.File) error { 27 | return syscall.Fsync(int(file.Fd())) 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stoolap/stoolap-go 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/marcboeker/go-duckdb v1.8.5 9 | github.com/mattn/go-sqlite3 v1.14.32 10 | ) 11 | 12 | require ( 13 | github.com/apache/arrow-go/v18 v18.1.0 // indirect 14 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 15 | github.com/goccy/go-json v0.10.5 // indirect 16 | github.com/google/flatbuffers v25.1.24+incompatible // indirect 17 | github.com/google/uuid v1.6.0 // indirect 18 | github.com/klauspost/compress v1.17.11 // indirect 19 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 20 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 21 | github.com/zeebo/xxh3 v1.0.2 // indirect 22 | golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect 23 | golang.org/x/mod v0.22.0 // indirect 24 | golang.org/x/sync v0.10.0 // indirect 25 | golang.org/x/sys v0.29.0 // indirect 26 | golang.org/x/tools v0.29.0 // indirect 27 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /test/merge_parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | func TestMergeParser(t *testing.T) { 25 | // Create a parser 26 | p := parser.NewParser(parser.NewLexer("MERGE INTO target USING source ON 1=1")) 27 | 28 | // Parse the program without validating to avoid panics 29 | p.ParseProgram() 30 | 31 | // Look at parse errors 32 | t.Logf("Parse errors: %v", p.Errors()) 33 | } 34 | -------------------------------------------------------------------------------- /examples/gormapp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 2 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 3 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 4 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 5 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 6 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 7 | github.com/stoolap/stoolap v0.0.2-beta h1:ie3zLyRR+33YT/7iL/h3sEBRxQZjFxgqOwHA+OzyuA0= 8 | github.com/stoolap/stoolap v0.0.2-beta/go.mod h1:MKu+ADLslhyAFMVzxb29dznlykALBFyZuMpBpNxRAQA= 9 | gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso= 10 | gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs= 11 | gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 12 | gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= 13 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 14 | -------------------------------------------------------------------------------- /internal/common/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package common 17 | 18 | const ( 19 | // VersionMajor is the major version of the driver 20 | VersionMajor = "0" 21 | // VersionMinor is the minor version of the driver 22 | VersionMinor = "1" 23 | // VersionPatch is the patch version of the driver 24 | VersionPatch = "3" 25 | // VersionSuffix is the suffix of the driver version 26 | VersionSuffix = "fb25a097" // git commit hash 27 | 28 | // VersionString is the version string of the driver 29 | VersionString = "Stoolap v" + VersionMajor + "." + VersionMinor + "." + VersionPatch + "-" + VersionSuffix 30 | ) 31 | -------------------------------------------------------------------------------- /test/memory_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" 23 | ) 24 | 25 | func TestMemoryDatabase(t *testing.T) { 26 | // Open an in-memory database 27 | db, err := sql.Open("stoolap", "memory://test") 28 | if err != nil { 29 | t.Fatalf("Failed to open database: %v", err) 30 | } 31 | defer db.Close() 32 | 33 | // Test ping 34 | if err := db.Ping(); err != nil { 35 | t.Fatalf("Failed to ping database: %v", err) 36 | } 37 | 38 | // TODO: Add more tests as functionality is implemented 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 1 31 | 32 | - name: Run Claude Code 33 | id: claude 34 | uses: anthropics/claude-code-action@beta 35 | with: 36 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 37 | 38 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: 12 | default: 13 | target: 70% 14 | threshold: 1% 15 | paths: 16 | - "internal/" 17 | - "pkg/" 18 | patch: 19 | default: 20 | target: 50% 21 | threshold: 1% 22 | 23 | parsers: 24 | gcov: 25 | branch_detection: 26 | conditional: yes 27 | loop: yes 28 | method: no 29 | macro: no 30 | 31 | comment: 32 | layout: "reach,diff,flags,files,footer" 33 | behavior: default 34 | require_changes: no 35 | 36 | ignore: 37 | - "test/" 38 | - "examples/" 39 | - "cmd/" 40 | - "**/*_test.go" 41 | - "**/mock_*.go" 42 | - "internal/storage/bitmap/" 43 | - "internal/storage/btree/" 44 | - "internal/storage/compression/" 45 | - "internal/parser/statement_clone.go" 46 | - "internal/btree/btree.go" 47 | - "internal/storage/binser/manager.go" 48 | - "internal/storage/binser/extensions.go" 49 | - "internal/storage/binser/statistics_metadata.go" 50 | - "internal/storage/binser/json_encoder.go" 51 | - "internal/sql/executor/streaming_join.go" 52 | - "internal/fastmap/int64_sync_map.go" 53 | - "internal/storage/mvcc/direct.go" 54 | - "internal/parser/sql_types.go" -------------------------------------------------------------------------------- /internal/storage/mvcc/file_lock_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | Copyright 2025 Stoolap Contributors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package mvcc 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "syscall" 24 | ) 25 | 26 | // acquireLock tries to acquire an exclusive lock on the file (Unix implementation) 27 | func acquireLock(file *os.File) error { 28 | err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) 29 | if err != nil { 30 | if err == syscall.EWOULDBLOCK { 31 | return fmt.Errorf("database is locked by another process") 32 | } 33 | return fmt.Errorf("failed to acquire lock: %w", err) 34 | } 35 | return nil 36 | } 37 | 38 | // releaseFileHandles is a no-op on Unix systems 39 | func releaseFileHandles() { 40 | // Unix systems don't need a delay after closing files 41 | } 42 | -------------------------------------------------------------------------------- /examples/gormapp/models.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "time" 20 | ) 21 | 22 | // User represents a user in the system 23 | type User struct { 24 | ID int64 `gorm:"primaryKey"` 25 | Name string `gorm:"size:255;not null"` 26 | Email string `gorm:"size:255;uniqueIndex"` 27 | Age int 28 | Active bool // Removed default value 29 | CreatedAt time.Time 30 | UpdatedAt time.Time 31 | } 32 | 33 | // Product represents a product in the inventory 34 | type Product struct { 35 | ID int64 `gorm:"primaryKey"` 36 | Name string `gorm:"size:255;not null"` 37 | Description string `gorm:"type:text"` 38 | Price float64 39 | Stock int // Removed default value 40 | CreatedAt time.Time 41 | UpdatedAt time.Time 42 | } 43 | -------------------------------------------------------------------------------- /examples/gormapp/README.md: -------------------------------------------------------------------------------- 1 | # Stoolap with GORM Sample Application 2 | 3 | This is a sample application demonstrating how to use GORM with Stoolap as the database backend. 4 | 5 | ## Features 6 | 7 | - Connects to Stoolap using the standard database/sql driver 8 | - Uses GORM for object-relational mapping 9 | - Demonstrates CRUD operations with two models: User and Product 10 | - Uses in-memory database for easy testing 11 | 12 | ## Running the Application 13 | 14 | To run the application: 15 | 16 | ```bash 17 | cd /path/to/stoolap/works/gormapp 18 | go mod tidy # Ensure all dependencies are installed 19 | go run . 20 | ``` 21 | 22 | ## Implementation Notes 23 | 24 | - This sample uses the MySQL dialect for GORM as a generic SQL dialect 25 | - It uses an in-memory Stoolap database (`memory://`) for demonstration 26 | - For a persistent database, change the DSN to `file:///path/to/database` 27 | 28 | ## Stoolap Compatibility Notes 29 | 30 | Since Stoolap has some SQL syntax differences from traditional databases, this sample application: 31 | 32 | 1. Creates tables manually instead of using GORM's AutoMigrate 33 | 2. Avoids default values in column definitions 34 | 3. Handles records one by one instead of batch operations 35 | 4. Uses simpler SQL commands that are supported by Stoolap 36 | 5. Adds the VERSION() function to Stoolap to support GORM's version checks 37 | 38 | This approach allows GORM to work with Stoolap while respecting its current SQL dialect limitations. 39 | -------------------------------------------------------------------------------- /internal/storage/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package storage 17 | 18 | import ( 19 | "sync" 20 | ) 21 | 22 | // EngineFactory is the interface for factories that create storage engines 23 | type EngineFactory interface { 24 | Create(url string) (Engine, error) 25 | } 26 | 27 | // Registry of engine factories 28 | var ( 29 | engineFactories = make(map[string]EngineFactory) 30 | factoryMutex sync.RWMutex 31 | ) 32 | 33 | // RegisterEngineFactory registers a storage engine factory 34 | func RegisterEngineFactory(name string, factory EngineFactory) { 35 | factoryMutex.Lock() 36 | defer factoryMutex.Unlock() 37 | engineFactories[name] = factory 38 | } 39 | 40 | // GetEngineFactory returns a storage engine factory 41 | func GetEngineFactory(name string) EngineFactory { 42 | factoryMutex.RLock() 43 | defer factoryMutex.RUnlock() 44 | return engineFactories[name] 45 | } 46 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 4 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 5 | github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= 6 | github.com/stoolap/stoolap v0.1.2/go.mod h1:MKu+ADLslhyAFMVzxb29dznlykALBFyZuMpBpNxRAQA= 7 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 8 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 9 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 10 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 11 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 12 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 13 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 14 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 15 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 16 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 17 | -------------------------------------------------------------------------------- /cmd/stoolap-pgserver/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var ( 26 | dbPath string 27 | bindAddr string 28 | verbose bool 29 | ) 30 | 31 | var rootCmd = &cobra.Command{ 32 | Use: "stoolap-pgserver", 33 | Short: "PostgreSQL wire protocol server for Stoolap", 34 | Long: `Stoolap PostgreSQL wire protocol server provides PostgreSQL compatibility, 35 | allowing any PostgreSQL client or driver to connect to a Stoolap database.`, 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | return runServer() 38 | }, 39 | } 40 | 41 | func init() { 42 | rootCmd.Flags().StringVarP(&dbPath, "database", "d", "memory://", "Database path (memory:// or file:///path/to/db)") 43 | rootCmd.Flags().StringVarP(&bindAddr, "bind", "b", ":5432", "Bind address") 44 | rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose logging") 45 | } 46 | 47 | func main() { 48 | if err := rootCmd.Execute(); err != nil { 49 | fmt.Fprintln(os.Stderr, err) 50 | os.Exit(1) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/parser/sql_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | // SqlType represents a SQL data type 19 | type SqlType int 20 | 21 | const ( 22 | // Invalid represents an invalid data type 23 | Invalid SqlType = iota 24 | // Integer represents an INTEGER data type 25 | Integer 26 | // Float represents a FLOAT data type 27 | Float 28 | // Text represents a TEXT data type 29 | Text 30 | // Boolean represents a BOOLEAN data type 31 | Boolean 32 | // Timestamp represents a TIMESTAMP data type 33 | Timestamp 34 | // Date represents a DATE data type 35 | Date 36 | // Time represents a TIME data type 37 | Time 38 | // Json represents a JSON data type 39 | Json 40 | ) 41 | 42 | // String returns a string representation of the SQL type 43 | func (s SqlType) String() string { 44 | switch s { 45 | case Integer: 46 | return "INTEGER" 47 | case Float: 48 | return "FLOAT" 49 | case Text: 50 | return "TEXT" 51 | case Boolean: 52 | return "BOOLEAN" 53 | case Timestamp: 54 | return "TIMESTAMP" 55 | case Date: 56 | return "DATE" 57 | case Time: 58 | return "TIME" 59 | case Json: 60 | return "JSON" 61 | default: 62 | return "INVALID" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/sql/executor/params.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package executor 17 | 18 | import ( 19 | "database/sql/driver" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | // parameter provides parameter substitution without modifying AST 25 | type parameter []driver.NamedValue 26 | 27 | // newParameter creates a new parameter 28 | func newParameter(params []driver.NamedValue) (*parameter, error) { 29 | if len(params) == 0 { 30 | return nil, nil 31 | } 32 | 33 | ps := parameter(params) 34 | 35 | return &ps, nil 36 | } 37 | 38 | // GetValue returns the literal value for a parameter 39 | func (ps parameter) GetValue(param *parser.Parameter) driver.NamedValue { 40 | if len(ps) == 0 { 41 | return driver.NamedValue{} 42 | } 43 | 44 | if param.OrderInStatement >= len(ps) { 45 | return driver.NamedValue{} 46 | } 47 | 48 | // OrderInStatement is 0-based 49 | if param.Index == 0 { 50 | return ps[param.OrderInStatement] 51 | } else { 52 | // For named parameters ($N style), use the Index directly 53 | // Get the substituted value 54 | for _, nv := range ps { 55 | if nv.Ordinal == param.Index { 56 | return nv 57 | } 58 | } 59 | } 60 | 61 | return driver.NamedValue{} 62 | } 63 | -------------------------------------------------------------------------------- /internal/parser/parse.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | import ( 19 | "strings" 20 | ) 21 | 22 | // Parse parses a SQL query and returns the parsed statement 23 | func Parse(query string) (Statement, error) { 24 | // Normalize the query - replace potential problem cases with standard SQL 25 | query = strings.TrimSpace(query) 26 | 27 | // Remove any trailing semicolons to avoid parser errors 28 | query = strings.TrimSuffix(query, ";") 29 | 30 | l := NewLexer(query) 31 | p := NewParser(l) 32 | 33 | program := p.ParseProgram() 34 | 35 | if len(p.Errors()) > 0 { 36 | return nil, &SQLParseError{ 37 | errors: p.Errors(), 38 | } 39 | } 40 | 41 | if len(program.Statements) == 0 { 42 | return nil, &SQLParseError{ 43 | errors: []string{"No statements found in query"}, 44 | } 45 | } 46 | 47 | // Return the first statement 48 | return program.Statements[0], nil 49 | } 50 | 51 | // SQLParseError represents a SQL parsing error 52 | type SQLParseError struct { 53 | errors []string 54 | } 55 | 56 | // Error returns the error message 57 | func (e *SQLParseError) Error() string { 58 | if len(e.errors) == 0 { 59 | return "SQL parse error" 60 | } 61 | return e.errors[0] 62 | } 63 | 64 | // Errors returns all parsing errors 65 | func (e *SQLParseError) Errors() []string { 66 | return e.errors 67 | } 68 | -------------------------------------------------------------------------------- /internal/parser/update_ast_tests.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | /* 4 | Copyright 2025 Stoolap Contributors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package main 19 | 20 | import ( 21 | "io/ioutil" 22 | "strings" 23 | ) 24 | 25 | // This script helps update AST test cases to use helper functions 26 | func main() { 27 | // Read the test file 28 | src, err := ioutil.ReadFile("ast_test.go") 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | content := string(src) 34 | 35 | // Define replacements 36 | replacements := []struct { 37 | old string 38 | new string 39 | }{ 40 | // Integer literals 41 | {`&IntegerLiteral{Value: `, `makeIntegerLiteral(`}, 42 | // Float literals 43 | {`&FloatLiteral{Value: `, `makeFloatLiteral(`}, 44 | // String literals 45 | {`&StringLiteral{Value: `, `makeStringLiteral(`}, 46 | // Boolean literals 47 | {`&BooleanLiteral{Value: `, `makeBooleanLiteral(`}, 48 | // Identifiers 49 | {`&Identifier{Value: `, `makeIdentifier(`}, 50 | // Fix closing braces 51 | {`makeIntegerLiteral(42}`, `makeIntegerLiteral(42)`}, 52 | {`makeFloatLiteral(3.14}`, `makeFloatLiteral(3.14)`}, 53 | } 54 | 55 | // Apply replacements 56 | for _, r := range replacements { 57 | content = strings.ReplaceAll(content, r.old, r.new) 58 | } 59 | 60 | // Write back 61 | err = ioutil.WriteFile("ast_test.go", []byte(content), 0644) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/simple_distinct_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/stoolap/stoolap-go" 24 | ) 25 | 26 | func TestSimpleDistinct(t *testing.T) { 27 | db, err := stoolap.Open("memory://") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | defer db.Close() 32 | 33 | ctx := context.Background() 34 | 35 | // Create test table 36 | _, err = db.Exec(ctx, ` 37 | CREATE TABLE test ( 38 | id INTEGER PRIMARY KEY, 39 | value TEXT 40 | ) 41 | `) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | // Insert duplicate values 47 | _, err = db.Exec(ctx, ` 48 | INSERT INTO test (id, value) VALUES 49 | (1, 'A'), 50 | (2, 'B'), 51 | (3, 'A'), 52 | (4, 'B'), 53 | (5, 'A'), 54 | (6, 'C') 55 | `) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | // Test DISTINCT 61 | query := `SELECT DISTINCT value FROM test ORDER BY value` 62 | rows, err := db.Query(ctx, query) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | defer rows.Close() 67 | 68 | var values []string 69 | for rows.Next() { 70 | var val string 71 | rows.Scan(&val) 72 | values = append(values, val) 73 | } 74 | 75 | fmt.Printf("DISTINCT values: %v\n", values) 76 | if len(values) != 3 { 77 | t.Fatalf("Expected 3 distinct values, got %d: %v", len(values), values) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/cte_expression_alias_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go" 23 | ) 24 | 25 | func TestCTEExpressionAlias(t *testing.T) { 26 | db, err := stoolap.Open("memory://") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer db.Close() 31 | 32 | ctx := context.Background() 33 | 34 | // Create test table 35 | _, err = db.Exec(ctx, ` 36 | CREATE TABLE test ( 37 | a INTEGER, 38 | b INTEGER 39 | ) 40 | `) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | // Insert test data 46 | _, err = db.Exec(ctx, ` 47 | INSERT INTO test (a, b) VALUES 48 | (10, 2), 49 | (30, 3) 50 | `) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | // Test: Expression with aliased columns 56 | query := ` 57 | WITH renamed (x, y) AS ( 58 | SELECT a, b FROM test 59 | ) 60 | SELECT x / y as result FROM renamed WHERE y > 0 61 | ` 62 | 63 | rows, err := db.Query(ctx, query) 64 | if err != nil { 65 | t.Fatalf("Query failed: %v", err) 66 | } 67 | defer rows.Close() 68 | 69 | var count int 70 | for rows.Next() { 71 | var result float64 72 | err := rows.Scan(&result) 73 | if err != nil { 74 | t.Fatalf("Scan failed: %v", err) 75 | } 76 | t.Logf("result=%f", result) 77 | count++ 78 | } 79 | if count != 2 { 80 | t.Errorf("Expected 2 rows, got %d", count) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/storage/bitmap/like_implementation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package bitmap 17 | 18 | import ( 19 | "strings" 20 | ) 21 | 22 | // GetMatchingLike returns a bitmap where the values match the SQL LIKE pattern 23 | func (idx *Index) GetMatchingLike(likePattern string) (*Bitmap, error) { 24 | idx.mutex.RLock() 25 | defer idx.mutex.RUnlock() 26 | 27 | // Create an empty result bitmap 28 | resultBitmap := New(idx.rowCount) 29 | 30 | // Check each value in the index 31 | for value, bitmap := range idx.valueMap { 32 | // For SQL standard compatibility, do case-insensitive comparison 33 | compareValue := strings.ToLower(value) 34 | pattern := strings.ToLower(likePattern) 35 | 36 | // Determine if there's a match 37 | var matches bool 38 | 39 | // In standard SQL, a pattern without wildcards is treated as an exact match 40 | if !strings.ContainsAny(pattern, "%_") { 41 | // No wildcards, use exact matching (standard SQL behavior) 42 | matches = compareValue == pattern 43 | } else { 44 | // Pattern has wildcards, use the pattern matching function 45 | matches = MatchSQLPattern(compareValue, pattern) 46 | } 47 | 48 | // If matches, set the corresponding bits in the result bitmap 49 | if matches { 50 | for i := int64(0); i < bitmap.Size(); i++ { 51 | val, _ := bitmap.Get(i) 52 | if val { 53 | resultBitmap.Set(i, true) 54 | } 55 | } 56 | } 57 | } 58 | 59 | return resultBitmap, nil 60 | } 61 | -------------------------------------------------------------------------------- /test/multi_statement_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | func TestMultiStatementParsing(t *testing.T) { 25 | // Test a multi-statement query with transaction statements 26 | multiStmt := ` 27 | BEGIN TRANSACTION; 28 | INSERT INTO users (id, name) VALUES (1, 'Alice'); 29 | COMMIT; 30 | ` 31 | 32 | p := parser.NewParser(parser.NewLexer(multiStmt)) 33 | program := p.ParseProgram() 34 | 35 | // Check that we have multiple statements 36 | if len(program.Statements) != 3 { 37 | t.Errorf("Expected 3 statements, got %d", len(program.Statements)) 38 | } 39 | 40 | // Check the types of statements 41 | if len(program.Statements) >= 3 { 42 | _, ok1 := program.Statements[0].(*parser.BeginStatement) 43 | _, ok2 := program.Statements[1].(*parser.InsertStatement) 44 | _, ok3 := program.Statements[2].(*parser.CommitStatement) 45 | 46 | if !ok1 { 47 | t.Errorf("First statement is not a BEGIN statement, got %T", program.Statements[0]) 48 | } 49 | 50 | if !ok2 { 51 | t.Errorf("Second statement is not an INSERT statement, got %T", program.Statements[1]) 52 | } 53 | 54 | if !ok3 { 55 | t.Errorf("Third statement is not a COMMIT statement, got %T", program.Statements[2]) 56 | } 57 | } 58 | 59 | // Check for any parse errors 60 | if len(p.Errors()) > 0 { 61 | t.Logf("Parse errors: %v", p.Errors()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/parser/ast_hash.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | // InExpressionHash is an optimized IN expression that uses a hash set for O(1) lookups 19 | // This is created by the query optimizer from regular InExpression when processing subqueries 20 | type InExpressionHash struct { 21 | Left Expression 22 | Not bool 23 | ValueSet map[interface{}]bool 24 | HasNull bool 25 | } 26 | 27 | // expressionNode marks this as an expression 28 | func (e *InExpressionHash) expressionNode() {} 29 | 30 | // TokenLiteral returns the token literal 31 | func (e *InExpressionHash) TokenLiteral() string { 32 | if e.Not { 33 | return "NOT IN" 34 | } 35 | return "IN" 36 | } 37 | 38 | // String returns the string representation 39 | func (e *InExpressionHash) String() string { 40 | result := e.Left.String() 41 | if e.Not { 42 | result += " NOT IN" 43 | } else { 44 | result += " IN" 45 | } 46 | result += " ()" 47 | return result 48 | } 49 | 50 | // Position returns the position of the node in the source code 51 | func (e *InExpressionHash) Position() Position { 52 | // This is a synthetic node created during optimization, so return zero position 53 | return Position{Line: 0, Column: 0} 54 | } 55 | 56 | // Accept implements the Visitor pattern 57 | // For now, this is a placeholder as InExpressionHash is an internal optimization 58 | func (e *InExpressionHash) Accept(v interface{}) { 59 | // No-op for now 60 | } 61 | -------------------------------------------------------------------------------- /test/date_time_simple_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "regexp" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func TestRegexMatching(t *testing.T) { 25 | // Test date regex 26 | dateRegex := `^\d{4}-\d{2}-\d{2}$` 27 | dateStr := "2023-05-15" 28 | 29 | matched, _ := regexp.MatchString(dateRegex, dateStr) 30 | if !matched { 31 | t.Errorf("Expected date string '%s' to match regex '%s'", dateStr, dateRegex) 32 | } 33 | 34 | // Test time regex 35 | timeRegex := `^\d{2}:\d{2}:\d{2}$` 36 | timeStr := "14:30:00" 37 | 38 | matched, _ = regexp.MatchString(timeRegex, timeStr) 39 | if !matched { 40 | t.Errorf("Expected time string '%s' to match regex '%s'", timeStr, timeRegex) 41 | } 42 | } 43 | 44 | func TestDateParsing(t *testing.T) { 45 | dateStr := "2023-05-15" 46 | date, err := time.Parse("2006-01-02", dateStr) 47 | if err != nil { 48 | t.Fatalf("Failed to parse date: %v", err) 49 | } 50 | 51 | expectedDate := time.Date(2023, 5, 15, 0, 0, 0, 0, time.UTC) 52 | if !date.Equal(expectedDate) { 53 | t.Errorf("Expected date %v but got %v", expectedDate, date) 54 | } 55 | } 56 | 57 | func TestTimeParsing(t *testing.T) { 58 | timeStr := "14:30:00" 59 | timeVal, err := time.Parse("15:04:05", timeStr) 60 | if err != nil { 61 | t.Fatalf("Failed to parse time: %v", err) 62 | } 63 | 64 | if timeVal.Hour() != 14 || timeVal.Minute() != 30 || timeVal.Second() != 0 { 65 | t.Errorf("Expected time 14:30:00 but got %d:%d:%d", 66 | timeVal.Hour(), timeVal.Minute(), timeVal.Second()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/hash_optimization_simple_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" 23 | ) 24 | 25 | // TestHashOptimizationSimple tests basic IN subquery functionality 26 | func TestHashOptimizationSimple(t *testing.T) { 27 | db, err := sql.Open("stoolap", "memory://") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | defer db.Close() 32 | 33 | // Create tables 34 | _, err = db.Exec(`CREATE TABLE t1 (id INTEGER)`) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | _, err = db.Exec(`CREATE TABLE t2 (id INTEGER)`) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | // Insert test data 45 | _, err = db.Exec(`INSERT INTO t1 VALUES (1), (2), (3), (4), (5)`) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | _, err = db.Exec(`INSERT INTO t2 VALUES (2), (4)`) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | // Test IN subquery 56 | rows, err := db.Query(`SELECT id FROM t1 WHERE id IN (SELECT id FROM t2)`) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer rows.Close() 61 | 62 | var results []int 63 | for rows.Next() { 64 | var id int 65 | err := rows.Scan(&id) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | results = append(results, id) 70 | } 71 | 72 | // Should return 2 and 4 73 | if len(results) != 2 { 74 | t.Errorf("Expected 2 results, got %d: %v", len(results), results) 75 | } 76 | if results[0] != 2 || results[1] != 4 { 77 | t.Errorf("Expected [2, 4], got %v", results) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/simple_time_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" 23 | ) 24 | 25 | func TestSimpleTimeFunctions(t *testing.T) { 26 | // Connect to the database 27 | db, err := sql.Open("stoolap", "memory://") 28 | if err != nil { 29 | t.Fatalf("Failed to open database: %v", err) 30 | } 31 | defer db.Close() 32 | 33 | // Create a test table 34 | _, err = db.Exec(` 35 | CREATE TABLE simple_time_test ( 36 | id INTEGER, 37 | event_time TIMESTAMP 38 | ) 39 | `) 40 | if err != nil { 41 | t.Fatalf("Failed to create test table: %v", err) 42 | } 43 | 44 | // Insert test data 45 | _, err = db.Exec(` 46 | INSERT INTO simple_time_test (id, event_time) VALUES 47 | (1, '2021-03-15T09:15:30') 48 | `) 49 | if err != nil { 50 | t.Fatalf("Failed to insert test data: %v", err) 51 | } 52 | 53 | // Test basic TIME_TRUNC 54 | rows, err := db.Query(` 55 | SELECT TIME_TRUNC('1h', event_time) FROM simple_time_test WHERE id = 1 56 | `) 57 | if err != nil { 58 | t.Fatalf("Failed to query: %v", err) 59 | } 60 | defer rows.Close() 61 | 62 | // Get column types 63 | cols, err := rows.Columns() 64 | if err != nil { 65 | t.Fatalf("Failed to get columns: %v", err) 66 | } 67 | 68 | t.Logf("Columns: %v", cols) 69 | 70 | // Scan a simple result 71 | if rows.Next() { 72 | var timeVal interface{} 73 | if err := rows.Scan(&timeVal); err != nil { 74 | t.Fatalf("Failed to scan row: %v", err) 75 | } 76 | 77 | t.Logf("TIME_TRUNC result: %v (type: %T)", timeVal, timeVal) 78 | } else { 79 | t.Fatalf("No rows returned") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/storage/mvcc/file_lock_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | Copyright 2025 Stoolap Contributors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package mvcc 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "syscall" 24 | "time" 25 | "unsafe" 26 | ) 27 | 28 | var ( 29 | modkernel32 = syscall.NewLazyDLL("kernel32.dll") 30 | procLockFileEx = modkernel32.NewProc("LockFileEx") 31 | procUnlockFileEx = modkernel32.NewProc("UnlockFileEx") 32 | ) 33 | 34 | const ( 35 | LOCKFILE_EXCLUSIVE_LOCK = 0x00000002 36 | LOCKFILE_FAIL_IMMEDIATELY = 0x00000001 37 | ERROR_LOCK_VIOLATION = 33 38 | ) 39 | 40 | type OVERLAPPED struct { 41 | Internal uintptr 42 | InternalHigh uintptr 43 | Offset uint32 44 | OffsetHigh uint32 45 | HEvent uintptr 46 | } 47 | 48 | // acquireLock tries to acquire an exclusive lock on the file (Windows implementation) 49 | func acquireLock(file *os.File) error { 50 | handle := syscall.Handle(file.Fd()) 51 | 52 | var overlapped OVERLAPPED 53 | 54 | ret, _, err := procLockFileEx.Call( 55 | uintptr(handle), 56 | uintptr(LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY), 57 | 0, 58 | 1, 59 | 0, 60 | uintptr(unsafe.Pointer(&overlapped)), 61 | ) 62 | 63 | if ret == 0 { 64 | if err == syscall.Errno(ERROR_LOCK_VIOLATION) { 65 | return fmt.Errorf("database is locked by another process") 66 | } 67 | return fmt.Errorf("failed to acquire lock: %w", err) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // releaseFileHandles is called after closing files on Windows to ensure handles are released 74 | func releaseFileHandles() { 75 | // Windows file handles may take a moment to be fully released 76 | // This small delay helps prevent "file in use" errors during cleanup 77 | time.Sleep(250 * time.Millisecond) 78 | } 79 | -------------------------------------------------------------------------------- /test/parameter_binding_benchmark_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "database/sql/driver" 21 | "testing" 22 | "time" 23 | 24 | "github.com/stoolap/stoolap-go" 25 | ) 26 | 27 | func BenchmarkParameterBinding(b *testing.B) { 28 | // Create a memory engine for testing 29 | db, err := stoolap.Open("memory://") 30 | if err != nil { 31 | b.Fatalf("Failed to create engine: %v", err) 32 | } 33 | defer db.Close() 34 | 35 | // Use the Executor method from DB 36 | exec := db.Executor() 37 | 38 | // Create a test table with different data types 39 | createTbl := "CREATE TABLE test_params (id INTEGER, name TEXT, salary FLOAT, active BOOLEAN, hire_date DATE, meta JSON)" 40 | result, err := exec.Execute(context.Background(), nil, createTbl) 41 | if err != nil { 42 | b.Fatalf("Failed to create test table: %v", err) 43 | } 44 | result.Close() 45 | 46 | // Parameters for typical insert 47 | typicalParams := []driver.NamedValue{ 48 | {Ordinal: 1, Value: 101}, 49 | {Ordinal: 2, Value: "Jane Smith"}, 50 | {Ordinal: 3, Value: 85000.75}, 51 | {Ordinal: 4, Value: false}, 52 | {Ordinal: 5, Value: time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)}, 53 | {Ordinal: 6, Value: map[string]interface{}{"department": "Engineering", "level": 3}}, 54 | } 55 | 56 | // Prepare the query with parameters for all columns 57 | query := "INSERT INTO test_params (id, name, salary, active, hire_date, meta) VALUES (?, ?, ?, ?, ?, ?)" 58 | 59 | b.ResetTimer() 60 | 61 | for i := 0; i < b.N; i++ { 62 | result, err := exec.ExecuteWithParams(context.Background(), nil, query, typicalParams) 63 | if err != nil { 64 | b.Fatalf("Failed to insert: %v", err) 65 | } 66 | result.Close() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/sql/executor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // package executor provides SQL execution functionality 17 | package sql 18 | 19 | import ( 20 | "context" 21 | "database/sql/driver" 22 | 23 | "github.com/stoolap/stoolap-go/internal/sql/executor" 24 | "github.com/stoolap/stoolap-go/internal/storage" 25 | ) 26 | 27 | // NewExecutor creates a new SQL executor 28 | func NewExecutor(engine storage.Engine) *Executor { 29 | // Create the real executor 30 | sqlExecutor := executor.NewExecutor(engine) 31 | 32 | return &Executor{ 33 | sqlExecutor: sqlExecutor, 34 | } 35 | } 36 | 37 | // Executor executes SQL statements 38 | type Executor struct { 39 | sqlExecutor *executor.Executor 40 | } 41 | 42 | // Execute executes a SQL statement 43 | func (e *Executor) Execute(ctx context.Context, tx storage.Transaction, query string) (storage.Result, error) { 44 | return e.sqlExecutor.ExecuteWithParams(ctx, tx, query, nil) 45 | } 46 | 47 | // ExecuteWithParams executes a SQL statement with parameters 48 | func (e *Executor) ExecuteWithParams(ctx context.Context, tx storage.Transaction, query string, params []driver.NamedValue) (storage.Result, error) { 49 | return e.sqlExecutor.ExecuteWithParams(ctx, tx, query, params) 50 | } 51 | 52 | // EnableVectorizedMode enables vectorized execution for appropriate query types 53 | func (e *Executor) EnableVectorizedMode() { 54 | e.sqlExecutor.EnableVectorizedMode() 55 | } 56 | 57 | // DisableVectorizedMode disables vectorized execution 58 | func (e *Executor) DisableVectorizedMode() { 59 | e.sqlExecutor.DisableVectorizedMode() 60 | } 61 | 62 | // IsVectorizedModeEnabled returns whether vectorized execution is enabled 63 | func (e *Executor) IsVectorizedModeEnabled() bool { 64 | return e.sqlExecutor.IsVectorizedModeEnabled() 65 | } 66 | -------------------------------------------------------------------------------- /internal/functions/scalar/now.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "time" 20 | 21 | "github.com/stoolap/stoolap-go/internal/functions/contract" 22 | "github.com/stoolap/stoolap-go/internal/functions/registry" 23 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 24 | ) 25 | 26 | // NowFunction implements the NOW() function 27 | type NowFunction struct{} 28 | 29 | // Name returns the name of the function 30 | func (f *NowFunction) Name() string { 31 | return "NOW" 32 | } 33 | 34 | // GetInfo returns the function information 35 | func (f *NowFunction) GetInfo() funcregistry.FunctionInfo { 36 | return funcregistry.FunctionInfo{ 37 | Name: "NOW", 38 | Type: funcregistry.ScalarFunction, 39 | Description: "Returns the current date and time", 40 | Signature: funcregistry.FunctionSignature{ 41 | ReturnType: funcregistry.TypeDateTime, 42 | ArgumentTypes: []funcregistry.DataType{}, 43 | MinArgs: 0, 44 | MaxArgs: 0, 45 | IsVariadic: false, 46 | }, 47 | } 48 | } 49 | 50 | // Register registers the function with the registry 51 | func (f *NowFunction) Register(registry funcregistry.Registry) { 52 | info := f.GetInfo() 53 | registry.MustRegister(info) 54 | } 55 | 56 | // Evaluate returns the current date and time 57 | func (f *NowFunction) Evaluate(args ...interface{}) (interface{}, error) { 58 | return time.Now(), nil 59 | } 60 | 61 | // NewNowFunction creates a new NOW function 62 | func NewNowFunction() contract.ScalarFunction { 63 | return &NowFunction{} 64 | } 65 | 66 | // Self-registration 67 | func init() { 68 | // Register the NOW function with the global registry 69 | if registry := registry.GetGlobal(); registry != nil { 70 | registry.RegisterScalarFunction(NewNowFunction()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/update_param_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | func TestUpdateParameters(t *testing.T) { 25 | query := "UPDATE users SET name = ?, age = ? WHERE id = ?" 26 | 27 | l := parser.NewLexer(query) 28 | p := parser.NewParser(l) 29 | program := p.ParseProgram() 30 | if len(p.Errors()) > 0 { 31 | t.Errorf("parser has %d errors for input %s", len(p.Errors()), query) 32 | for _, err := range p.Errors() { 33 | t.Errorf("parser error: %s", err) 34 | } 35 | return 36 | } 37 | 38 | // Print the statement as parsed 39 | t.Logf("Parsed statement: %s", program.Statements[0].String()) 40 | 41 | // Extract all parameters with their location information 42 | var params []*parser.Parameter 43 | extractParams(program.Statements[0], ¶ms) 44 | 45 | for i, param := range params { 46 | t.Logf("Parameter %d: Location=%s, StatementID=%d, OrderInStatement=%d", 47 | i+1, param.Location, param.StatementID, param.OrderInStatement) 48 | } 49 | } 50 | 51 | func extractParams(node parser.Node, params *[]*parser.Parameter) { 52 | switch n := node.(type) { 53 | case *parser.Parameter: 54 | *params = append(*params, n) 55 | case *parser.UpdateStatement: 56 | // Check the Updates map 57 | for _, expr := range n.Updates { 58 | extractParams(expr, params) 59 | } 60 | // Check the WHERE clause 61 | if n.Where != nil { 62 | extractParams(n.Where, params) 63 | } 64 | case *parser.InfixExpression: 65 | if n.Left != nil { 66 | extractParams(n.Left, params) 67 | } 68 | if n.Right != nil { 69 | extractParams(n.Right, params) 70 | } 71 | case *parser.PrefixExpression: 72 | if n.Right != nil { 73 | extractParams(n.Right, params) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/cte_exists_totals_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go" 23 | ) 24 | 25 | func TestCTETotals(t *testing.T) { 26 | db, err := stoolap.Open("memory://") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer db.Close() 31 | 32 | ctx := context.Background() 33 | 34 | // Create test table 35 | _, err = db.Exec(ctx, ` 36 | CREATE TABLE sales ( 37 | id INTEGER PRIMARY KEY, 38 | product TEXT, 39 | region TEXT, 40 | amount FLOAT, 41 | year INTEGER 42 | ) 43 | `) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | // Insert test data 49 | _, err = db.Exec(ctx, ` 50 | INSERT INTO sales (id, product, region, amount, year) VALUES 51 | (1, 'Widget', 'North', 1000, 2023), 52 | (2, 'Widget', 'South', 1500, 2023), 53 | (3, 'Gadget', 'North', 2000, 2023), 54 | (4, 'Gadget', 'South', 2500, 2023), 55 | (5, 'Widget', 'North', 1200, 2024), 56 | (6, 'Widget', 'South', 1800, 2024), 57 | (7, 'Gadget', 'North', 2200, 2024), 58 | (8, 'Gadget', 'South', 2800, 2024) 59 | `) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | // Check totals per product 65 | t.Run("Product totals", func(t *testing.T) { 66 | query := ` 67 | SELECT product, SUM(amount) as total 68 | FROM sales 69 | GROUP BY product 70 | ` 71 | 72 | rows, err := db.Query(ctx, query) 73 | if err != nil { 74 | t.Fatalf("Query failed: %v", err) 75 | } 76 | defer rows.Close() 77 | 78 | t.Log("Product totals:") 79 | for rows.Next() { 80 | var product string 81 | var total float64 82 | err := rows.Scan(&product, &total) 83 | if err != nil { 84 | t.Fatalf("Scan failed: %v", err) 85 | } 86 | t.Logf(" product=%s, total=%f", product, total) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /test/cast_fix_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go" 23 | ) 24 | 25 | // TestCastFix tests a direct fix for the CAST expression issue in WHERE clauses 26 | func TestCastFix(t *testing.T) { 27 | // Create a test database 28 | ctx := context.Background() 29 | db, _ := stoolap.Open("memory://") 30 | defer db.Close() 31 | 32 | executor := db.Executor() 33 | 34 | // Create test table and insert data 35 | executor.Execute(ctx, nil, ` 36 | CREATE TABLE cast_fix_test ( 37 | id INTEGER PRIMARY KEY, 38 | text_val TEXT 39 | ) 40 | `) 41 | 42 | executor.Execute(ctx, nil, ` 43 | INSERT INTO cast_fix_test (id, text_val) VALUES 44 | (1, '123'), 45 | (2, '456') 46 | `) 47 | 48 | // Test direct CAST in SELECT - this should work 49 | result, _ := executor.Execute(ctx, nil, `SELECT CAST(text_val AS INTEGER) FROM cast_fix_test WHERE id = 1`) 50 | if result.Next() { 51 | var castVal int64 52 | result.Scan(&castVal) 53 | if castVal != 123 { 54 | t.Errorf("Expected CAST(text_val AS INTEGER) to be 123, got %d", castVal) 55 | } else { 56 | t.Logf("CAST in SELECT works: CAST(text_val AS INTEGER) = %d", castVal) 57 | } 58 | } 59 | 60 | // Test WHERE with CAST - this is what we're fixing 61 | whereResult, _ := executor.Execute(ctx, nil, ` 62 | SELECT id FROM cast_fix_test WHERE CAST(text_val AS INTEGER) > 200 63 | `) 64 | 65 | // Count matches 66 | matches := 0 67 | for whereResult.Next() { 68 | var id int64 69 | whereResult.Scan(&id) 70 | t.Logf("CAST in WHERE works: row id=%d matches CAST(text_val AS INTEGER) > 200", id) 71 | matches++ 72 | } 73 | 74 | // We expect only one match (id=2, text_val=456) 75 | if matches != 1 { 76 | t.Errorf("Expected 1 match for WHERE CAST(text_val AS INTEGER) > 200, got %d", matches) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/parser/cast_parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | import ( 19 | "fmt" 20 | "testing" 21 | ) 22 | 23 | func TestParseCastExpression(t *testing.T) { 24 | // Test direct parsing of a full statement with CAST expressions 25 | tests := []struct { 26 | input string 27 | exprType string 28 | }{ 29 | {"SELECT CAST(123 AS INTEGER);", "INTEGER"}, 30 | {"SELECT CAST('hello' AS TEXT);", "TEXT"}, 31 | {"SELECT CAST(column_name AS FLOAT);", "FLOAT"}, 32 | {"SELECT CAST(NULL AS BOOLEAN);", "BOOLEAN"}, 33 | {"SELECT CAST(date_col AS DATE);", "DATE"}, 34 | {"SELECT CAST(json_col AS JSON);", "JSON"}, 35 | } 36 | 37 | for i, tt := range tests { 38 | t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) { 39 | l := NewLexer(tt.input) 40 | p := NewParser(l) 41 | program := p.ParseProgram() 42 | 43 | if len(p.Errors()) > 0 { 44 | t.Errorf("parser has %d errors for input %s", len(p.Errors()), tt.input) 45 | for _, err := range p.Errors() { 46 | t.Errorf("parser error: %s", err) 47 | } 48 | return 49 | } 50 | 51 | if len(program.Statements) != 1 { 52 | t.Fatalf("program does not have 1 statement. got=%d", len(program.Statements)) 53 | } 54 | 55 | selectStmt, ok := program.Statements[0].(*SelectStatement) 56 | if !ok { 57 | t.Fatalf("program.Statements[0] is not SelectStatement. got=%T", program.Statements[0]) 58 | } 59 | 60 | if len(selectStmt.Columns) != 1 { 61 | t.Fatalf("select statement doesn't have 1 column, got=%d", len(selectStmt.Columns)) 62 | } 63 | 64 | castExpr, ok := selectStmt.Columns[0].(*CastExpression) 65 | if !ok { 66 | t.Fatalf("column is not CastExpression. got=%T", selectStmt.Columns[0]) 67 | } 68 | 69 | if castExpr.TypeName != tt.exprType { 70 | t.Errorf("castExpr.TypeName not %s. got=%s", tt.exprType, castExpr.TypeName) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/common/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Package common provides shared types and utilities 17 | package common 18 | 19 | import ( 20 | "sync" 21 | 22 | "github.com/stoolap/stoolap-go/internal/storage" 23 | ) 24 | 25 | // Global sync.Pool for map[string]storage.ColumnValue to reduce allocations 26 | var ( 27 | SmallColumnValueMapPool = &sync.Pool{ 28 | New: func() interface{} { 29 | return make(map[string]storage.ColumnValue, 8) // For small number of columns 30 | }, 31 | } 32 | 33 | MediumColumnValueMapPool = &sync.Pool{ 34 | New: func() interface{} { 35 | return make(map[string]storage.ColumnValue, 32) // For medium number of columns 36 | }, 37 | } 38 | 39 | LargeColumnValueMapPool = &sync.Pool{ 40 | New: func() interface{} { 41 | return make(map[string]storage.ColumnValue, 64) // For large number of columns 42 | }, 43 | } 44 | ) 45 | 46 | // GetColumnValueMapPool returns the appropriate map pool based on column count 47 | func GetColumnValueMapPool(columnCount int) *sync.Pool { 48 | if columnCount <= 8 { 49 | return SmallColumnValueMapPool 50 | } else if columnCount <= 32 { 51 | return MediumColumnValueMapPool 52 | } 53 | return LargeColumnValueMapPool 54 | } 55 | 56 | // PutColumnValueMap returns a map to the appropriate pool 57 | func PutColumnValueMap(m map[string]storage.ColumnValue, columnCount int) { 58 | // Clear the map before returning it to the pool 59 | clear(m) 60 | 61 | // Return to the appropriate pool based on size 62 | pool := GetColumnValueMapPool(columnCount) 63 | pool.Put(m) 64 | } 65 | 66 | // GetColumnValueMap gets a map from the appropriate pool based on expected size 67 | func GetColumnValueMap(columnCount int) map[string]storage.ColumnValue { 68 | pool := GetColumnValueMapPool(columnCount) 69 | m := pool.Get().(map[string]storage.ColumnValue) 70 | 71 | // Map should already be cleared when returned to pool, but clear it just in case 72 | clear(m) 73 | 74 | return m 75 | } 76 | -------------------------------------------------------------------------------- /internal/sql/executor/temporal.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package executor 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | "github.com/stoolap/stoolap-go/internal/storage" 23 | ) 24 | 25 | // TemporalContext holds AS OF information for temporal queries 26 | type TemporalContext struct { 27 | Type string // "TRANSACTION" or "TIMESTAMP" 28 | Value any // int64 for TRANSACTION, time.Time for TIMESTAMP 29 | } 30 | 31 | // extractTemporalContext extracts AS OF information from a table source 32 | func extractTemporalContext(tableSource *parser.SimpleTableSource) (*TemporalContext, error) { 33 | if tableSource.AsOf == nil { 34 | return nil, nil // No temporal context 35 | } 36 | 37 | // Extract the value based on type 38 | switch tableSource.AsOf.Type { 39 | case "TRANSACTION": 40 | // For AS OF TRANSACTION, expect an integer literal 41 | switch v := tableSource.AsOf.Value.(type) { 42 | case *parser.IntegerLiteral: 43 | return &TemporalContext{ 44 | Type: "TRANSACTION", 45 | Value: v.Value, 46 | }, nil 47 | default: 48 | return nil, fmt.Errorf("AS OF TRANSACTION requires integer value, got %T", v) 49 | } 50 | 51 | case "TIMESTAMP": 52 | // For AS OF TIMESTAMP, expect a string literal that can be parsed as timestamp 53 | switch v := tableSource.AsOf.Value.(type) { 54 | case *parser.StringLiteral: 55 | // Use the existing ParseTimestamp function from converters.go 56 | ts, err := storage.ParseTimestamp(v.Value) 57 | if err != nil { 58 | return nil, fmt.Errorf("invalid timestamp in AS OF clause: %w", err) 59 | } 60 | // Return the parsed time.Time object 61 | return &TemporalContext{ 62 | Type: "TIMESTAMP", 63 | Value: ts, 64 | }, nil 65 | default: 66 | return nil, fmt.Errorf("AS OF TIMESTAMP requires string value, got %T", v) 67 | } 68 | 69 | default: 70 | return nil, fmt.Errorf("unsupported AS OF type: %s", tableSource.AsOf.Type) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/storage/mvcc/file_lock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package mvcc 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | ) 23 | 24 | // FileLock represents an exclusive lock on a database directory 25 | type FileLock struct { 26 | file *os.File 27 | path string 28 | } 29 | 30 | // AcquireFileLock attempts to acquire an exclusive lock on the database directory. 31 | // It creates a lock file in the database directory and locks it using OS-level file locking. 32 | // Returns an error if the lock cannot be acquired (typically because another process has it). 33 | func AcquireFileLock(dbPath string) (*FileLock, error) { 34 | // Ensure the directory exists 35 | if err := os.MkdirAll(dbPath, 0755); err != nil { 36 | return nil, fmt.Errorf("failed to create database directory: %w", err) 37 | } 38 | 39 | // Lock file path 40 | lockFilePath := filepath.Join(dbPath, "db.lock") 41 | 42 | // Open the lock file (create if it doesn't exist) 43 | file, err := os.OpenFile(lockFilePath, os.O_CREATE|os.O_RDWR, 0644) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to open lock file: %w", err) 46 | } 47 | 48 | // Try to acquire an exclusive lock (platform-specific implementation) 49 | if err = acquireLock(file); err != nil { 50 | file.Close() 51 | return nil, err 52 | } 53 | 54 | // Write the current process ID to the lock file for debugging 55 | pid := os.Getpid() 56 | file.Truncate(0) 57 | file.Seek(0, 0) 58 | fmt.Fprintf(file, "%d", pid) 59 | 60 | return &FileLock{ 61 | file: file, 62 | path: lockFilePath, 63 | }, nil 64 | } 65 | 66 | // Release releases the file lock 67 | func (l *FileLock) Release() error { 68 | if l.file == nil { 69 | return nil 70 | } 71 | 72 | // Release the lock by closing the file 73 | err := l.file.Close() 74 | l.file = nil 75 | 76 | // We don't remove the lock file as it will be reused on next open 77 | // This helps preserve the lock file permissions between runs 78 | 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /test/json_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/storage" 22 | ) 23 | 24 | func TestJSONParsing(t *testing.T) { 25 | // Test parsing JSON through the storage package functions 26 | // This doesn't rely on the SQL parser, just on the storage conversion functions 27 | 28 | // Test with a simple JSON object 29 | jsonStr := `{"name":"John","age":30}` 30 | result := storage.ConvertStorageValueToGoValue(jsonStr, storage.JSON) 31 | 32 | // With our current implementation, we're just returning the string as is 33 | if result != jsonStr { 34 | t.Errorf("Expected JSON string to be returned as is, got %v", result) 35 | } 36 | 37 | // Test with a JSON array 38 | jsonArray := `[1,2,3,4]` 39 | result = storage.ConvertStorageValueToGoValue(jsonArray, storage.JSON) 40 | 41 | if result != jsonArray { 42 | t.Errorf("Expected JSON array to be returned as is, got %v", result) 43 | } 44 | 45 | // Test with a map (already parsed JSON) 46 | jsonMap := map[string]interface{}{ 47 | "name": "John", 48 | "age": 30, 49 | } 50 | result = storage.ConvertStorageValueToGoValue(jsonMap, storage.JSON) 51 | 52 | // Since maps can't be directly compared in Go, we need to compare their contents 53 | resultMap, ok := result.(map[string]interface{}) 54 | if !ok { 55 | t.Errorf("Expected map but got %T", result) 56 | return 57 | } 58 | 59 | // Check map size 60 | if len(jsonMap) != len(resultMap) { 61 | t.Errorf("Expected map of size %d but got %d", len(jsonMap), len(resultMap)) 62 | return 63 | } 64 | 65 | // Check each key-value pair 66 | for k, v := range jsonMap { 67 | if resultVal, exists := resultMap[k]; exists { 68 | // For simplicity, just check if the string representation matches 69 | if resultVal != v { 70 | t.Errorf("For key %s, expected %v but got %v", k, v, resultVal) 71 | } 72 | } else { 73 | t.Errorf("Key %s not found in result map", k) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/string_quotes_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | func TestStringQuoteRemoval(t *testing.T) { 25 | tests := []struct { 26 | input string 27 | expected string 28 | expectedType parser.TokenType 29 | }{ 30 | // Single quotes create string tokens 31 | {"'simple string'", "simple string", parser.TokenString}, 32 | // Double quotes now create identifier tokens (SQL standard) 33 | {"\"double quoted\"", "double quoted", parser.TokenIdentifier}, 34 | {"'2023-05-15'", "2023-05-15", parser.TokenString}, 35 | {"'14:30:00'", "14:30:00", parser.TokenString}, 36 | // Add backtick test for completeness 37 | {"`backtick id`", "backtick id", parser.TokenIdentifier}, 38 | } 39 | 40 | for i, tt := range tests { 41 | l := parser.NewLexer(tt.input) 42 | // For each string, we'll just check how the lexer tokenizes it 43 | token := l.NextToken() 44 | 45 | // Check the token type 46 | if token.Type != tt.expectedType { 47 | t.Fatalf("Test %d: Expected %v but got %v", i, tt.expectedType, token.Type) 48 | } 49 | 50 | // Check the literal based on token type 51 | if token.Type == parser.TokenString { 52 | // String tokens include quotes 53 | if token.Literal != tt.input { 54 | t.Errorf("Test %d: Expected literal '%s' but got '%s'", i, tt.input, token.Literal) 55 | } 56 | // Extract content without quotes 57 | content := "" 58 | if len(token.Literal) >= 2 { 59 | content = token.Literal[1 : len(token.Literal)-1] 60 | } 61 | if content != tt.expected { 62 | t.Errorf("Test %d: Expected content '%s' but got '%s'", i, tt.expected, content) 63 | } 64 | } else if token.Type == parser.TokenIdentifier { 65 | // Identifier tokens don't include quotes 66 | if token.Literal != tt.expected { 67 | t.Errorf("Test %d: Expected identifier '%s' but got '%s'", i, tt.expected, token.Literal) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/common/test_helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package common 18 | 19 | import ( 20 | "os" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | // TempDir creates a temporary directory that is cleaned up when the test ends. 28 | // Unlike t.TempDir(), this ensures cleanup happens with proper timing for Windows. 29 | // It uses t.Cleanup() to register cleanup that runs before t.TempDir() cleanup. 30 | func TempDir(t *testing.T) string { 31 | t.Helper() 32 | 33 | // Create temp dir with test name for uniqueness 34 | // Replace path separators with underscores to avoid issues with subtest names 35 | testName := t.Name() 36 | for _, char := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} { 37 | testName = strings.ReplaceAll(testName, char, "_") 38 | } 39 | prefix := "stoolap-" + testName + "-" 40 | dir, err := os.MkdirTemp("", prefix) 41 | if err != nil { 42 | t.Fatalf("failed to create temp dir: %v", err) 43 | } 44 | 45 | // Register cleanup with retry logic 46 | t.Cleanup(func() { 47 | // On Windows, add a small delay before cleanup 48 | if runtime.GOOS == "windows" { 49 | time.Sleep(100 * time.Millisecond) 50 | } 51 | 52 | // Try to remove with retries for Windows 53 | maxRetries := 1 54 | if runtime.GOOS == "windows" { 55 | maxRetries = 5 56 | } 57 | 58 | var lastErr error 59 | for i := 0; i < maxRetries; i++ { 60 | err := os.RemoveAll(dir) 61 | if err == nil { 62 | return 63 | } 64 | lastErr = err 65 | 66 | // On Windows, wait before retry with exponential backoff 67 | if runtime.GOOS == "windows" && i < maxRetries-1 { 68 | time.Sleep(time.Duration(100*(i+1)) * time.Millisecond) 69 | } 70 | } 71 | 72 | // If we still can't remove it, log but don't fail the test 73 | // This is common on Windows due to antivirus or other processes 74 | if lastErr != nil { 75 | t.Logf("Warning: failed to remove temp dir %q after %d attempts: %v", dir, maxRetries, lastErr) 76 | } 77 | }) 78 | 79 | return dir 80 | } 81 | -------------------------------------------------------------------------------- /internal/storage/mvcc/mvcc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package mvcc 17 | 18 | import ( 19 | "errors" 20 | "sync/atomic" 21 | "time" 22 | ) 23 | 24 | // These errors represent common issues in MVCC operations 25 | var ( 26 | ErrMVCCNoTransaction = errors.New("no transaction provided") 27 | ErrMVCCPKViolation = errors.New("primary key violation: duplicate key") 28 | ErrMVCCInvalidTable = errors.New("invalid or unknown table") 29 | ErrMVCCInvalidRow = errors.New("invalid row data") 30 | ) 31 | 32 | // Timestamp generation system that guarantees monotonically increasing values 33 | // even under heavy concurrent usage 34 | var ( 35 | lastTimestamp atomic.Int64 // Last timestamp value 36 | seqNum atomic.Int64 // Sequence number for timestamp collisions 37 | ) 38 | 39 | // GetFastTimestamp returns a monotonically increasing timestamp 40 | // suitable for transaction ordering and version tracking 41 | func GetFastTimestamp() int64 { 42 | // Get current time in nanoseconds 43 | nowNano := time.Now().UnixNano() 44 | 45 | for { 46 | // Load current last timestamp 47 | lastTS := lastTimestamp.Load() 48 | 49 | // If current time is greater than last timestamp, update using CAS 50 | if nowNano > lastTS { 51 | // Try to update the timestamp using CAS to avoid race conditions 52 | if lastTimestamp.CompareAndSwap(lastTS, nowNano) { 53 | // Reset sequence number when timestamp changes 54 | seqNum.Store(0) 55 | return nowNano 56 | } 57 | } else { 58 | // If we have a timestamp collision or system clock went backwards: 59 | // Use the last timestamp and increment a sequence number in the low bits 60 | // Get a unique sequence within this timestamp 61 | seq := seqNum.Add(1) 62 | 63 | // Create a composite timestamp by replacing the low 10 bits with sequence 64 | // This allows 1024 unique values per nanosecond if needed 65 | compositeTS := (lastTS & ^int64(0x3FF)) | (seq & 0x3FF) 66 | 67 | return compositeTS 68 | } 69 | 70 | // If CAS failed, loop and try again 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/functions/scalar/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "github.com/stoolap/stoolap-go/internal/common" 20 | "github.com/stoolap/stoolap-go/internal/functions/contract" 21 | "github.com/stoolap/stoolap-go/internal/functions/registry" 22 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 23 | ) 24 | 25 | // VersionFunction implements the VERSION() function 26 | // This function is commonly used by database tools and ORMs to check database compatibility 27 | type VersionFunction struct{} 28 | 29 | // Name returns the name of the function 30 | func (f *VersionFunction) Name() string { 31 | return "VERSION" 32 | } 33 | 34 | // GetInfo returns the function information 35 | func (f *VersionFunction) GetInfo() funcregistry.FunctionInfo { 36 | return funcregistry.FunctionInfo{ 37 | Name: "VERSION", 38 | Type: funcregistry.ScalarFunction, 39 | Description: "Returns the Stoolap database version string", 40 | Signature: funcregistry.FunctionSignature{ 41 | ReturnType: funcregistry.TypeString, 42 | ArgumentTypes: []funcregistry.DataType{}, 43 | MinArgs: 0, 44 | MaxArgs: 0, 45 | IsVariadic: false, 46 | }, 47 | } 48 | } 49 | 50 | // Register registers the function with the registry 51 | func (f *VersionFunction) Register(registry funcregistry.Registry) { 52 | info := f.GetInfo() 53 | registry.MustRegister(info) 54 | } 55 | 56 | // Evaluate returns the Stoolap version string 57 | func (f *VersionFunction) Evaluate(args ...interface{}) (interface{}, error) { 58 | // Version string for Stoolap - can be updated as needed 59 | return common.VersionString, nil 60 | } 61 | 62 | // NewVersionFunction creates a new VERSION function 63 | func NewVersionFunction() contract.ScalarFunction { 64 | return &VersionFunction{} 65 | } 66 | 67 | // Self-registration 68 | func init() { 69 | // Register the VERSION function with the global registry 70 | if registry := registry.GetGlobal(); registry != nil { 71 | registry.RegisterScalarFunction(NewVersionFunction()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/functions/window/row_number.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package window 17 | 18 | import ( 19 | "github.com/stoolap/stoolap-go/internal/functions/contract" 20 | "github.com/stoolap/stoolap-go/internal/functions/registry" 21 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 22 | ) 23 | 24 | // RowNumberFunction implements the ROW_NUMBER() window function 25 | type RowNumberFunction struct{} 26 | 27 | // Name returns the name of the function 28 | func (f *RowNumberFunction) Name() string { 29 | return "ROW_NUMBER" 30 | } 31 | 32 | // GetInfo returns the function information 33 | func (f *RowNumberFunction) GetInfo() funcregistry.FunctionInfo { 34 | return funcregistry.FunctionInfo{ 35 | Name: "ROW_NUMBER", 36 | Type: funcregistry.WindowFunction, 37 | Description: "Returns the sequential row number within the current partition", 38 | Signature: funcregistry.FunctionSignature{ 39 | ReturnType: funcregistry.TypeInteger, 40 | ArgumentTypes: []funcregistry.DataType{}, 41 | MinArgs: 0, 42 | MaxArgs: 0, 43 | IsVariadic: false, 44 | }, 45 | } 46 | } 47 | 48 | // Register registers the function with the registry 49 | func (f *RowNumberFunction) Register(registry funcregistry.Registry) { 50 | info := f.GetInfo() 51 | registry.MustRegister(info) 52 | } 53 | 54 | // Process returns the row number within the current partition 55 | func (f *RowNumberFunction) Process(partition []interface{}, orderBy []interface{}) (interface{}, error) { 56 | // For ROW_NUMBER(), we don't use any specific column values 57 | // We just return the position in the partition (1-based) 58 | return int64(1), nil 59 | } 60 | 61 | // NewRowNumberFunction creates a new ROW_NUMBER function 62 | func NewRowNumberFunction() contract.WindowFunction { 63 | return &RowNumberFunction{} 64 | } 65 | 66 | // Self-registration 67 | func init() { 68 | // Register the ROW_NUMBER function with the global registry 69 | if registry := registry.GetGlobal(); registry != nil { 70 | registry.RegisterWindowFunction(NewRowNumberFunction()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/functions/scalar/concat.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "strings" 20 | 21 | "github.com/stoolap/stoolap-go/internal/functions/contract" 22 | "github.com/stoolap/stoolap-go/internal/functions/registry" 23 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 24 | ) 25 | 26 | // ConcatFunction implements the CONCAT function 27 | type ConcatFunction struct{} 28 | 29 | // Name returns the name of the function 30 | func (f *ConcatFunction) Name() string { 31 | return "CONCAT" 32 | } 33 | 34 | // GetInfo returns the function information 35 | func (f *ConcatFunction) GetInfo() funcregistry.FunctionInfo { 36 | return funcregistry.FunctionInfo{ 37 | Name: "CONCAT", 38 | Type: funcregistry.ScalarFunction, 39 | Description: "Concatenates strings", 40 | Signature: funcregistry.FunctionSignature{ 41 | ReturnType: funcregistry.TypeString, 42 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 43 | MinArgs: 1, 44 | MaxArgs: -1, // unlimited 45 | IsVariadic: true, 46 | }, 47 | } 48 | } 49 | 50 | // Register registers the function with the registry 51 | func (f *ConcatFunction) Register(registry funcregistry.Registry) { 52 | info := f.GetInfo() 53 | registry.MustRegister(info) 54 | } 55 | 56 | // Evaluate concatenates strings 57 | func (f *ConcatFunction) Evaluate(args ...interface{}) (interface{}, error) { 58 | if len(args) == 0 { 59 | return "", nil 60 | } 61 | 62 | var sb strings.Builder 63 | for _, arg := range args { 64 | if arg != nil { 65 | sb.WriteString(ConvertToString(arg)) 66 | } 67 | } 68 | 69 | return sb.String(), nil 70 | } 71 | 72 | // NewConcatFunction creates a new CONCAT function 73 | func NewConcatFunction() contract.ScalarFunction { 74 | return &ConcatFunction{} 75 | } 76 | 77 | // Self-registration 78 | func init() { 79 | // Register the CONCAT function with the global registry 80 | // This happens automatically when the package is imported 81 | if registry := registry.GetGlobal(); registry != nil { 82 | registry.RegisterScalarFunction(NewConcatFunction()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/functions/scalar/length.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/stoolap/stoolap-go/internal/functions/contract" 22 | "github.com/stoolap/stoolap-go/internal/functions/registry" 23 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 24 | ) 25 | 26 | // LengthFunction implements the LENGTH function 27 | type LengthFunction struct{} 28 | 29 | // Name returns the name of the function 30 | func (f *LengthFunction) Name() string { 31 | return "LENGTH" 32 | } 33 | 34 | // GetInfo returns the function information 35 | func (f *LengthFunction) GetInfo() funcregistry.FunctionInfo { 36 | return funcregistry.FunctionInfo{ 37 | Name: "LENGTH", 38 | Type: funcregistry.ScalarFunction, 39 | Description: "Returns the length of a string", 40 | Signature: funcregistry.FunctionSignature{ 41 | ReturnType: funcregistry.TypeInteger, 42 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 43 | MinArgs: 1, 44 | MaxArgs: 1, 45 | IsVariadic: false, 46 | }, 47 | } 48 | } 49 | 50 | // Register registers the function with the registry 51 | func (f *LengthFunction) Register(registry funcregistry.Registry) { 52 | info := f.GetInfo() 53 | registry.MustRegister(info) 54 | } 55 | 56 | // Evaluate returns the length of a string 57 | func (f *LengthFunction) Evaluate(args ...interface{}) (interface{}, error) { 58 | if len(args) != 1 { 59 | return nil, fmt.Errorf("LENGTH requires exactly 1 argument, got %d", len(args)) 60 | } 61 | 62 | if args[0] == nil { 63 | return nil, nil 64 | } 65 | 66 | str := ConvertToString(args[0]) 67 | return int64(len(str)), nil 68 | } 69 | 70 | // NewLengthFunction creates a new LENGTH function 71 | func NewLengthFunction() contract.ScalarFunction { 72 | return &LengthFunction{} 73 | } 74 | 75 | // Self-registration 76 | func init() { 77 | // Register the LENGTH function with the global registry 78 | // This happens automatically when the package is imported 79 | if registry := registry.GetGlobal(); registry != nil { 80 | registry.RegisterScalarFunction(NewLengthFunction()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/functions/scalar/lower.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/contract" 23 | "github.com/stoolap/stoolap-go/internal/functions/registry" 24 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 25 | ) 26 | 27 | // LowerFunction implements the LOWER function 28 | type LowerFunction struct{} 29 | 30 | // Name returns the name of the function 31 | func (f *LowerFunction) Name() string { 32 | return "LOWER" 33 | } 34 | 35 | // GetInfo returns the function information 36 | func (f *LowerFunction) GetInfo() funcregistry.FunctionInfo { 37 | return funcregistry.FunctionInfo{ 38 | Name: "LOWER", 39 | Type: funcregistry.ScalarFunction, 40 | Description: "Converts a string to lowercase", 41 | Signature: funcregistry.FunctionSignature{ 42 | ReturnType: funcregistry.TypeString, 43 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 44 | MinArgs: 1, 45 | MaxArgs: 1, 46 | IsVariadic: false, 47 | }, 48 | } 49 | } 50 | 51 | // Register registers the function with the registry 52 | func (f *LowerFunction) Register(registry funcregistry.Registry) { 53 | info := f.GetInfo() 54 | registry.MustRegister(info) 55 | } 56 | 57 | // Evaluate converts the string to lowercase 58 | func (f *LowerFunction) Evaluate(args ...interface{}) (interface{}, error) { 59 | if len(args) != 1 { 60 | return nil, fmt.Errorf("LOWER requires exactly 1 argument, got %d", len(args)) 61 | } 62 | 63 | if args[0] == nil { 64 | return nil, nil 65 | } 66 | 67 | str := ConvertToString(args[0]) 68 | return strings.ToLower(str), nil 69 | } 70 | 71 | // NewLowerFunction creates a new LOWER function 72 | func NewLowerFunction() contract.ScalarFunction { 73 | return &LowerFunction{} 74 | } 75 | 76 | // Self-registration 77 | func init() { 78 | // Register the LOWER function with the global registry 79 | // This happens automatically when the package is imported 80 | if registry := registry.GetGlobal(); registry != nil { 81 | registry.RegisterScalarFunction(NewLowerFunction()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/functions/scalar/upper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/contract" 23 | "github.com/stoolap/stoolap-go/internal/functions/registry" 24 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 25 | ) 26 | 27 | // UpperFunction implements the UPPER function 28 | type UpperFunction struct{} 29 | 30 | // Name returns the name of the function 31 | func (f *UpperFunction) Name() string { 32 | return "UPPER" 33 | } 34 | 35 | // GetInfo returns the function information 36 | func (f *UpperFunction) GetInfo() funcregistry.FunctionInfo { 37 | return funcregistry.FunctionInfo{ 38 | Name: "UPPER", 39 | Type: funcregistry.ScalarFunction, 40 | Description: "Converts a string to uppercase", 41 | Signature: funcregistry.FunctionSignature{ 42 | ReturnType: funcregistry.TypeString, 43 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 44 | MinArgs: 1, 45 | MaxArgs: 1, 46 | IsVariadic: false, 47 | }, 48 | } 49 | } 50 | 51 | // Register registers the function with the registry 52 | func (f *UpperFunction) Register(registry funcregistry.Registry) { 53 | info := f.GetInfo() 54 | registry.MustRegister(info) 55 | } 56 | 57 | // Evaluate converts the string to uppercase 58 | func (f *UpperFunction) Evaluate(args ...interface{}) (interface{}, error) { 59 | if len(args) != 1 { 60 | return nil, fmt.Errorf("UPPER requires exactly 1 argument, got %d", len(args)) 61 | } 62 | 63 | if args[0] == nil { 64 | return nil, nil 65 | } 66 | 67 | str := ConvertToString(args[0]) 68 | return strings.ToUpper(str), nil 69 | } 70 | 71 | // NewUpperFunction creates a new UPPER function 72 | func NewUpperFunction() contract.ScalarFunction { 73 | return &UpperFunction{} 74 | } 75 | 76 | // Self-registration 77 | func init() { 78 | // Register the UPPER function with the global registry 79 | // This happens automatically when the package is imported 80 | if registry := registry.GetGlobal(); registry != nil { 81 | registry.RegisterScalarFunction(NewUpperFunction()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/parser/cast_parser.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | // parseCastExpression parses a CAST expression 24 | func (p *Parser) parseCastExpression() Expression { 25 | // Save the current token (CAST keyword) 26 | castToken := p.curToken 27 | 28 | // Create a CAST expression with the current token 29 | expression := &CastExpression{ 30 | Token: castToken, 31 | } 32 | 33 | // Expect opening parenthesis 34 | if !p.expectPeek(TokenPunctuator) || p.curToken.Literal != "(" { 35 | p.addError(fmt.Sprintf("expected '(' after CAST, got %s at %s", p.curToken.Literal, p.curToken.Position)) 36 | return nil 37 | } 38 | 39 | // Move to the first token inside the parentheses 40 | p.nextToken() 41 | 42 | // Check for end of file 43 | if p.curTokenIs(TokenEOF) { 44 | p.addError(fmt.Sprintf("unexpected end of file in CAST expression at %s", p.curToken.Position)) 45 | return nil 46 | } 47 | 48 | // Parse the expression to cast 49 | expression.Expr = p.parseExpression(LOWEST) 50 | if expression.Expr == nil { 51 | p.addError(fmt.Sprintf("expected expression in CAST at %s", p.curToken.Position)) 52 | return nil 53 | } 54 | 55 | // Expect AS keyword 56 | if !p.expectPeek(TokenKeyword) || strings.ToUpper(p.curToken.Literal) != "AS" { 57 | p.addError(fmt.Sprintf("expected AS in CAST, got %s at %s", p.curToken.Literal, p.curToken.Position)) 58 | return nil 59 | } 60 | 61 | // Parse the type name 62 | if p.peekTokenIs(TokenKeyword) { 63 | p.nextToken() 64 | } else if p.peekTokenIs(TokenIdentifier) { 65 | p.nextToken() 66 | } else { 67 | p.addError(fmt.Sprintf("expected type name after AS in CAST, got %s at %s", p.peekToken.Literal, p.peekToken.Position)) 68 | return nil 69 | } 70 | 71 | // Store the type name 72 | expression.TypeName = p.curToken.Literal 73 | 74 | // Expect closing parenthesis 75 | if !p.expectPeek(TokenPunctuator) || p.curToken.Literal != ")" { 76 | p.addError(fmt.Sprintf("expected ')' after type name in CAST, got %s at %s", p.curToken.Literal, p.curToken.Position)) 77 | return nil 78 | } 79 | 80 | return expression 81 | } 82 | -------------------------------------------------------------------------------- /internal/sql/executor/columnar_aggregate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package executor 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/stoolap/stoolap-go/internal/storage" 22 | ) 23 | 24 | // OptimizedAggregateOnColumnar performs aggregation using columnar operations 25 | func OptimizedAggregateOnColumnar(cr *ColumnarResult, functions []*SqlFunction) (storage.Result, error) { 26 | // For simple aggregations without GROUP BY, we can use columnar operations 27 | resultValues := make([]interface{}, len(functions)) 28 | 29 | for i, fn := range functions { 30 | switch fn.Name { 31 | case "COUNT": 32 | if fn.Column == "*" { 33 | resultValues[i] = cr.CountColumnar() 34 | } else if fn.IsDistinct { 35 | count, err := cr.DistinctCountColumnar(fn.Column) 36 | if err != nil { 37 | return nil, err 38 | } 39 | resultValues[i] = count 40 | } else { 41 | count, err := cr.CountNonNull(fn.Column) 42 | if err != nil { 43 | return nil, err 44 | } 45 | resultValues[i] = count 46 | } 47 | case "MIN": 48 | min, err := cr.MinColumnar(fn.Column) 49 | if err != nil { 50 | return nil, err 51 | } 52 | resultValues[i] = min.AsInterface() 53 | case "MAX": 54 | max, err := cr.MaxColumnar(fn.Column) 55 | if err != nil { 56 | return nil, err 57 | } 58 | resultValues[i] = max.AsInterface() 59 | case "SUM": 60 | sum, err := cr.SumColumnar(fn.Column) 61 | if err != nil { 62 | return nil, err 63 | } 64 | resultValues[i] = sum.AsInterface() 65 | case "AVG": 66 | avg, err := cr.AvgColumnar(fn.Column) 67 | if err != nil { 68 | return nil, err 69 | } 70 | resultValues[i] = avg.AsInterface() 71 | default: 72 | return nil, fmt.Errorf("unsupported aggregate function for columnar: %s", fn.Name) 73 | } 74 | } 75 | 76 | // Create result columns 77 | resultColumns := make([]string, len(functions)) 78 | for i, fn := range functions { 79 | resultColumns[i] = fn.GetColumnName() 80 | } 81 | 82 | // Return a single-row result 83 | return &ExecResult{ 84 | columns: resultColumns, 85 | rows: [][]interface{}{resultValues}, 86 | isMemory: true, 87 | }, nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/storage/expression/not_expression.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package expression 17 | 18 | import ( 19 | "github.com/stoolap/stoolap-go/internal/storage" 20 | ) 21 | 22 | // NotExpression represents a logical NOT of an expression 23 | type NotExpression struct { 24 | Expr storage.Expression 25 | 26 | // Schema optimization 27 | isOptimized bool // Indicates if this expression has already been prepared for a schema 28 | } 29 | 30 | // NewNotExpression creates a new NOT expression 31 | func NewNotExpression(expr storage.Expression) *NotExpression { 32 | return &NotExpression{ 33 | Expr: expr, 34 | } 35 | } 36 | 37 | // Evaluate implements the Expression interface for NOT expressions 38 | func (e *NotExpression) Evaluate(row storage.Row) (bool, error) { 39 | result, err := e.Expr.Evaluate(row) 40 | if err != nil { 41 | return false, err 42 | } 43 | return !result, nil 44 | } 45 | 46 | // WithAliases implements the Expression interface 47 | func (e *NotExpression) WithAliases(aliases map[string]string) storage.Expression { 48 | // Apply aliases to the inner expression 49 | if aliasable, ok := e.Expr.(interface { 50 | WithAliases(map[string]string) storage.Expression 51 | }); ok { 52 | // Create a new expression with the aliased inner expression 53 | return &NotExpression{ 54 | Expr: aliasable.WithAliases(aliases), 55 | } 56 | } 57 | 58 | // If inner expression doesn't support aliases, return a copy of this expression 59 | return &NotExpression{ 60 | Expr: e.Expr, 61 | } 62 | } 63 | 64 | // PrepareForSchema optimizes the expression for a specific schema 65 | func (e *NotExpression) PrepareForSchema(schema storage.Schema) storage.Expression { 66 | // If already optimized, don't redo the work 67 | if e.isOptimized { 68 | return e 69 | } 70 | 71 | // Optimize the inner expression 72 | e.Expr = e.Expr.PrepareForSchema(schema) 73 | e.isOptimized = true 74 | 75 | return e 76 | } 77 | 78 | // EvaluateFast implements the Expression interface for fast evaluation 79 | func (e *NotExpression) EvaluateFast(row storage.Row) bool { 80 | // Simply negate the result of the inner expression's fast evaluation 81 | return !e.Expr.EvaluateFast(row) 82 | } 83 | -------------------------------------------------------------------------------- /internal/sql/executor/qualified_result.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package executor 17 | 18 | import ( 19 | "context" 20 | 21 | "github.com/stoolap/stoolap-go/internal/storage" 22 | ) 23 | 24 | // QualifiedResult wraps a result to add table qualification to column names 25 | type QualifiedResult struct { 26 | baseResult storage.Result 27 | tableAlias string 28 | columns []string 29 | } 30 | 31 | // NewQualifiedResult creates a new qualified result 32 | func NewQualifiedResult(baseResult storage.Result, tableAlias string) *QualifiedResult { 33 | baseColumns := baseResult.Columns() 34 | qualifiedColumns := make([]string, len(baseColumns)) 35 | 36 | for i, col := range baseColumns { 37 | qualifiedColumns[i] = tableAlias + "." + col 38 | } 39 | 40 | return &QualifiedResult{ 41 | baseResult: baseResult, 42 | tableAlias: tableAlias, 43 | columns: qualifiedColumns, 44 | } 45 | } 46 | 47 | // Columns returns the qualified column names 48 | func (r *QualifiedResult) Columns() []string { 49 | return r.columns 50 | } 51 | 52 | // Next advances to the next row 53 | func (r *QualifiedResult) Next() bool { 54 | return r.baseResult.Next() 55 | } 56 | 57 | // Row returns the current row 58 | func (r *QualifiedResult) Row() storage.Row { 59 | return r.baseResult.Row() 60 | } 61 | 62 | // Scan copies column values to destinations 63 | func (r *QualifiedResult) Scan(dest ...interface{}) error { 64 | return r.baseResult.Scan(dest...) 65 | } 66 | 67 | // Close closes the result 68 | func (r *QualifiedResult) Close() error { 69 | return r.baseResult.Close() 70 | } 71 | 72 | // RowsAffected returns rows affected 73 | func (r *QualifiedResult) RowsAffected() int64 { 74 | return r.baseResult.RowsAffected() 75 | } 76 | 77 | // LastInsertID returns last insert ID 78 | func (r *QualifiedResult) LastInsertID() int64 { 79 | return r.baseResult.LastInsertID() 80 | } 81 | 82 | // Context returns the context 83 | func (r *QualifiedResult) Context() context.Context { 84 | return r.baseResult.Context() 85 | } 86 | 87 | // WithAliases returns self as columns are already qualified 88 | func (r *QualifiedResult) WithAliases(aliases map[string]string) storage.Result { 89 | return r 90 | } 91 | -------------------------------------------------------------------------------- /cmd/stoolap-pgserver/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 5 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 6 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 7 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 8 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 9 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 10 | github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= 11 | github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 12 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 13 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 17 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 18 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 19 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 20 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 21 | github.com/stoolap/stoolap v0.1.1 h1:iNBhvd9pGL62M+9HIkdyXZIYnBMqv9VkD13t0RwdW7U= 22 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 23 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 24 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 25 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 26 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 27 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 28 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /test/parser_drop_index_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go/internal/sql" 23 | "github.com/stoolap/stoolap-go/internal/storage" 24 | 25 | // Import necessary packages to register factory functions 26 | _ "github.com/stoolap/stoolap-go/internal/storage/mvcc" 27 | ) 28 | 29 | // TestDropIndexIfExists tests the DROP INDEX IF EXISTS statement 30 | func TestDropIndexIfExists(t *testing.T) { 31 | // Get the block storage engine factory 32 | factory := storage.GetEngineFactory("mvcc") 33 | if factory == nil { 34 | t.Fatalf("Failed to get db engine factory") 35 | } 36 | 37 | // Create the engine with the connection string 38 | engine, err := factory.Create("memory://") 39 | if err != nil { 40 | t.Fatalf("Failed to create db engine: %v", err) 41 | } 42 | 43 | // Open the engine 44 | if err := engine.Open(); err != nil { 45 | t.Fatalf("Failed to open engine: %v", err) 46 | } 47 | defer engine.Close() 48 | 49 | // Create a SQL executor 50 | executor := sql.NewExecutor(engine) 51 | 52 | // Create a test table 53 | result, err := executor.Execute(context.Background(), nil, ` 54 | CREATE TABLE test_drop_index ( 55 | id INTEGER, 56 | name TEXT 57 | ) 58 | `) 59 | if err != nil { 60 | t.Fatalf("Failed to create test table: %v", err) 61 | } 62 | if result != nil { 63 | result.Close() 64 | } 65 | 66 | // Try to drop a nonexistent index with IF EXISTS (should succeed) 67 | result, err = executor.Execute(context.Background(), nil, ` 68 | DROP INDEX IF EXISTS nonexistent_idx ON test_drop_index 69 | `) 70 | if err != nil { 71 | t.Fatalf("DROP INDEX IF EXISTS failed: %v", err) 72 | } 73 | if result != nil { 74 | result.Close() 75 | } 76 | 77 | // Try to drop a nonexistent index without IF EXISTS (should fail) 78 | result, err = executor.Execute(context.Background(), nil, ` 79 | DROP INDEX nonexistent_idx ON test_drop_index 80 | `) 81 | if err == nil { 82 | if result != nil { 83 | result.Close() 84 | } 85 | t.Fatalf("Expected error when dropping nonexistent index without IF EXISTS, but got none") 86 | } 87 | 88 | t.Logf("DROP INDEX IF EXISTS test passed!") 89 | } 90 | -------------------------------------------------------------------------------- /internal/functions/scalar/abs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "math" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/contract" 23 | "github.com/stoolap/stoolap-go/internal/functions/registry" 24 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 25 | ) 26 | 27 | // AbsFunction implements the ABS function 28 | type AbsFunction struct{} 29 | 30 | // Name returns the name of the function 31 | func (f *AbsFunction) Name() string { 32 | return "ABS" 33 | } 34 | 35 | // GetInfo returns the function information 36 | func (f *AbsFunction) GetInfo() funcregistry.FunctionInfo { 37 | return funcregistry.FunctionInfo{ 38 | Name: "ABS", 39 | Type: funcregistry.ScalarFunction, 40 | Description: "Returns the absolute value of a number", 41 | Signature: funcregistry.FunctionSignature{ 42 | ReturnType: funcregistry.TypeFloat, 43 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 44 | MinArgs: 1, 45 | MaxArgs: 1, 46 | IsVariadic: false, 47 | }, 48 | } 49 | } 50 | 51 | // Register registers the function with the registry 52 | func (f *AbsFunction) Register(registry funcregistry.Registry) { 53 | info := f.GetInfo() 54 | registry.MustRegister(info) 55 | } 56 | 57 | // Evaluate returns the absolute value of a number 58 | func (f *AbsFunction) Evaluate(args ...interface{}) (interface{}, error) { 59 | if len(args) != 1 { 60 | return nil, fmt.Errorf("ABS requires exactly 1 argument, got %d", len(args)) 61 | } 62 | 63 | if args[0] == nil { 64 | return nil, nil 65 | } 66 | 67 | num, err := ConvertToFloat64(args[0]) 68 | if err != nil { 69 | return nil, fmt.Errorf("invalid number: %v", err) 70 | } 71 | 72 | return math.Abs(num), nil 73 | } 74 | 75 | // NewAbsFunction creates a new ABS function 76 | func NewAbsFunction() contract.ScalarFunction { 77 | return &AbsFunction{} 78 | } 79 | 80 | // Self-registration 81 | func init() { 82 | // Register the ABS function with the global registry 83 | // This happens automatically when the package is imported 84 | if registry := registry.GetGlobal(); registry != nil { 85 | registry.RegisterScalarFunction(NewAbsFunction()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/functions/scalar/floor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "math" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/contract" 23 | "github.com/stoolap/stoolap-go/internal/functions/registry" 24 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 25 | ) 26 | 27 | // FloorFunction implements the FLOOR function 28 | type FloorFunction struct{} 29 | 30 | // Name returns the name of the function 31 | func (f *FloorFunction) Name() string { 32 | return "FLOOR" 33 | } 34 | 35 | // GetInfo returns the function information 36 | func (f *FloorFunction) GetInfo() funcregistry.FunctionInfo { 37 | return funcregistry.FunctionInfo{ 38 | Name: "FLOOR", 39 | Type: funcregistry.ScalarFunction, 40 | Description: "Returns the largest integer value not greater than the argument", 41 | Signature: funcregistry.FunctionSignature{ 42 | ReturnType: funcregistry.TypeFloat, 43 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 44 | MinArgs: 1, 45 | MaxArgs: 1, 46 | IsVariadic: false, 47 | }, 48 | } 49 | } 50 | 51 | // Register registers the function with the registry 52 | func (f *FloorFunction) Register(registry funcregistry.Registry) { 53 | info := f.GetInfo() 54 | registry.MustRegister(info) 55 | } 56 | 57 | // Evaluate returns the floor of a number 58 | func (f *FloorFunction) Evaluate(args ...interface{}) (interface{}, error) { 59 | if len(args) != 1 { 60 | return nil, fmt.Errorf("FLOOR requires exactly 1 argument, got %d", len(args)) 61 | } 62 | 63 | if args[0] == nil { 64 | return nil, nil 65 | } 66 | 67 | num, err := ConvertToFloat64(args[0]) 68 | if err != nil { 69 | return nil, fmt.Errorf("invalid number: %v", err) 70 | } 71 | 72 | return math.Floor(num), nil 73 | } 74 | 75 | // NewFloorFunction creates a new FLOOR function 76 | func NewFloorFunction() contract.ScalarFunction { 77 | return &FloorFunction{} 78 | } 79 | 80 | // Self-registration 81 | func init() { 82 | // Register the FLOOR function with the global registry 83 | // This happens automatically when the package is imported 84 | if registry := registry.GetGlobal(); registry != nil { 85 | registry.RegisterScalarFunction(NewFloorFunction()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/parser/errors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | import ( 19 | "strings" 20 | "testing" 21 | ) 22 | 23 | func TestUserFriendlyErrors(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | sql string 27 | expectError bool 28 | errorContains []string 29 | }{ 30 | { 31 | name: "Unclosed parenthesis", 32 | sql: "SELECT * FROM users WHERE (id > 5", 33 | expectError: true, 34 | errorContains: []string{ 35 | "expected next token to be PUNCTUATOR", 36 | "A punctuation character like", 37 | }, 38 | }, 39 | { 40 | name: "Good query - no error", 41 | sql: "SELECT id, name FROM users WHERE age > 18", 42 | expectError: false, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | l := NewLexer(tt.sql) 49 | p := NewParser(l) 50 | 51 | // Parse and check for errors 52 | p.ParseProgram() 53 | hasErrors := len(p.Errors()) > 0 54 | 55 | if tt.expectError != hasErrors { 56 | t.Fatalf("Expected error: %v, got: %v", tt.expectError, hasErrors) 57 | } 58 | 59 | if !tt.expectError { 60 | return 61 | } 62 | 63 | // Format errors and check formatting 64 | formattedError := p.FormatErrors() 65 | if !strings.Contains(formattedError, "SQL parsing failed") { 66 | t.Errorf("Formatted error doesn't contain header: %s", formattedError) 67 | } 68 | 69 | // Check that the formatted error contains the original SQL 70 | if !strings.Contains(formattedError, tt.sql) { 71 | t.Errorf("Formatted error doesn't contain the original SQL: %s", formattedError) 72 | } 73 | 74 | // Verify error contains expected substrings 75 | for _, expected := range tt.errorContains { 76 | if !strings.Contains(formattedError, expected) { 77 | t.Errorf("Formatted error doesn't contain expected text '%s': %s", expected, formattedError) 78 | } 79 | } 80 | 81 | // Verify pointer indicator is present 82 | if !strings.Contains(formattedError, "^") { 83 | t.Errorf("Formatted error doesn't contain pointer indicator: %s", formattedError) 84 | } 85 | 86 | t.Logf("Formatted error (for visual inspection):\n%s", formattedError) 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/cte_simple_alias_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go" 23 | ) 24 | 25 | func TestCTESimpleAlias(t *testing.T) { 26 | db, err := stoolap.Open("memory://") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer db.Close() 31 | 32 | ctx := context.Background() 33 | 34 | // Create test table 35 | _, err = db.Exec(ctx, ` 36 | CREATE TABLE test ( 37 | a INTEGER, 38 | b INTEGER 39 | ) 40 | `) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | // Insert test data 46 | _, err = db.Exec(ctx, ` 47 | INSERT INTO test (a, b) VALUES 48 | (10, 20), 49 | (30, 40) 50 | `) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | // Test 1: Simple alias without WHERE 56 | t.Run("Simple alias", func(t *testing.T) { 57 | query := ` 58 | WITH renamed (x, y) AS ( 59 | SELECT a, b FROM test 60 | ) 61 | SELECT x + y as sum FROM renamed 62 | ` 63 | 64 | rows, err := db.Query(ctx, query) 65 | if err != nil { 66 | t.Fatalf("Query failed: %v", err) 67 | } 68 | defer rows.Close() 69 | 70 | var count int 71 | for rows.Next() { 72 | var sum int 73 | err := rows.Scan(&sum) 74 | if err != nil { 75 | t.Fatalf("Scan failed: %v", err) 76 | } 77 | t.Logf("sum=%d", sum) 78 | count++ 79 | } 80 | if count != 2 { 81 | t.Errorf("Expected 2 rows, got %d", count) 82 | } 83 | }) 84 | 85 | // Test 2: With WHERE on aliased column 86 | t.Run("With WHERE", func(t *testing.T) { 87 | query := ` 88 | WITH renamed (x, y) AS ( 89 | SELECT a, b FROM test 90 | ) 91 | SELECT x, y FROM renamed WHERE x > 20 92 | ` 93 | 94 | rows, err := db.Query(ctx, query) 95 | if err != nil { 96 | t.Fatalf("Query failed: %v", err) 97 | } 98 | defer rows.Close() 99 | 100 | var count int 101 | for rows.Next() { 102 | var x, y int 103 | err := rows.Scan(&x, &y) 104 | if err != nil { 105 | t.Fatalf("Scan failed: %v", err) 106 | } 107 | t.Logf("x=%d, y=%d", x, y) 108 | count++ 109 | } 110 | if count != 1 { 111 | t.Errorf("Expected 1 row, got %d", count) 112 | } 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /test/mvcc_simple_sum_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" 23 | ) 24 | 25 | // TestSimpleSumIsolation tests basic SUM behavior 26 | func TestSimpleSumIsolation(t *testing.T) { 27 | db, err := sql.Open("stoolap", "memory://") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | defer db.Close() 32 | 33 | // Create simple table 34 | _, err = db.Exec(`CREATE TABLE test (id INT PRIMARY KEY, val INT)`) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | // Insert data 40 | _, err = db.Exec(`INSERT INTO test VALUES (1, 100), (2, 200)`) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | // Test 1: Simple SUM 46 | var sum int 47 | err = db.QueryRow("SELECT SUM(val) FROM test").Scan(&sum) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | if sum != 300 { 52 | t.Errorf("Expected sum 300, got %d", sum) 53 | } 54 | 55 | // Test 2: SUM in transaction 56 | tx, err := db.Begin() 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer tx.Rollback() 61 | 62 | var txSum int 63 | err = tx.QueryRow("SELECT SUM(val) FROM test").Scan(&txSum) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | if txSum != 300 { 68 | t.Errorf("Expected sum 300 in transaction, got %d", txSum) 69 | } 70 | } 71 | 72 | // TestSumAfterUpdate tests SUM after updates 73 | func TestSumAfterUpdate(t *testing.T) { 74 | db, err := sql.Open("stoolap", "memory://") 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | defer db.Close() 79 | 80 | // Setup 81 | _, err = db.Exec(`CREATE TABLE test (id INT PRIMARY KEY, val INT)`) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | _, err = db.Exec(`INSERT INTO test VALUES (1, 100), (2, 200)`) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | // Update one row 91 | _, err = db.Exec("UPDATE test SET val = 150 WHERE id = 1") 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | // Check sum 97 | var sum int 98 | err = db.QueryRow("SELECT SUM(val) FROM test").Scan(&sum) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | if sum != 350 { // 150 + 200 103 | t.Errorf("Expected sum 350 after update, got %d", sum) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/as_of_parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | func TestAsOfParser(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | input string 28 | expected string 29 | wantErr bool 30 | }{ 31 | { 32 | name: "AS OF TRANSACTION with integer", 33 | input: "SELECT * FROM users AS OF TRANSACTION 12345", 34 | expected: "SELECT * FROM users AS OF TRANSACTION 12345", 35 | }, 36 | { 37 | name: "AS OF TIMESTAMP with string", 38 | input: "SELECT * FROM users AS OF TIMESTAMP '2024-01-01 10:00:00'", 39 | expected: "SELECT * FROM users AS OF TIMESTAMP '2024-01-01 10:00:00'", 40 | }, 41 | { 42 | name: "AS OF with table alias", 43 | input: "SELECT * FROM users AS OF TRANSACTION 100 AS u", 44 | expected: "SELECT * FROM users AS OF TRANSACTION 100 AS u", 45 | }, 46 | { 47 | name: "AS OF with implicit alias", 48 | input: "SELECT * FROM users AS OF TIMESTAMP '2024-01-01' u", 49 | expected: "SELECT * FROM users AS OF TIMESTAMP '2024-01-01' AS u", 50 | }, 51 | { 52 | name: "AS OF in JOIN", 53 | input: "SELECT * FROM users AS OF TRANSACTION 100 u JOIN orders o ON u.id = o.user_id", 54 | expected: "SELECT * FROM users AS OF TRANSACTION 100 AS u INNER JOIN orders AS o ON (u.id = o.user_id)", 55 | }, 56 | { 57 | name: "Invalid AS OF type", 58 | input: "SELECT * FROM users AS OF INVALID 100", 59 | wantErr: true, 60 | }, 61 | { 62 | name: "Missing AS OF value", 63 | input: "SELECT * FROM users AS OF TRANSACTION", 64 | wantErr: true, 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | lexer := parser.NewLexer(tt.input) 71 | p := parser.NewParser(lexer) 72 | program := p.ParseProgram() 73 | 74 | if tt.wantErr { 75 | if len(p.Errors()) == 0 { 76 | t.Errorf("expected error but got none") 77 | } 78 | return 79 | } 80 | 81 | if len(p.Errors()) > 0 { 82 | t.Fatalf("parser errors: %v", p.Errors()) 83 | } 84 | 85 | if len(program.Statements) == 0 { 86 | t.Fatalf("no statements parsed") 87 | } 88 | 89 | got := program.Statements[0].String() 90 | if got != tt.expected { 91 | t.Errorf("got %q, want %q", got, tt.expected) 92 | } 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/parser/view_parser.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | import ( 19 | "fmt" 20 | ) 21 | 22 | // parseCreateViewStatement parses a CREATE VIEW statement 23 | func (p *Parser) parseCreateViewStatement() *CreateViewStatement { 24 | // Create a new CREATE VIEW statement with the CREATE token 25 | stmt := &CreateViewStatement{ 26 | Token: p.curToken, 27 | } 28 | 29 | // Check for optional IF NOT EXISTS 30 | if p.peekTokenIsKeyword("IF") { 31 | p.nextToken() // Consume IF 32 | 33 | if !p.expectKeyword("NOT") { 34 | return nil 35 | } 36 | 37 | if !p.expectKeyword("EXISTS") { 38 | return nil 39 | } 40 | 41 | stmt.IfNotExists = true 42 | } 43 | 44 | // Parse view name 45 | if !p.expectPeek(TokenIdentifier) { 46 | p.addError(fmt.Sprintf("expected identifier as view name, got %s at %s", p.peekToken.Literal, p.peekToken.Position)) 47 | return nil 48 | } 49 | 50 | // Create view name identifier 51 | stmt.ViewName = &Identifier{ 52 | Token: p.curToken, 53 | Value: p.curToken.Literal, 54 | } 55 | 56 | // Expect AS keyword 57 | if !p.expectKeyword("AS") { 58 | return nil 59 | } 60 | 61 | // Expect SELECT keyword 62 | if !p.expectKeyword("SELECT") { 63 | return nil 64 | } 65 | 66 | // Parse the SELECT statement 67 | selectStmt := p.parseSelectStatement() 68 | if selectStmt == nil { 69 | return nil 70 | } 71 | 72 | stmt.Query = selectStmt 73 | 74 | return stmt 75 | } 76 | 77 | // parseDropViewStatement parses a DROP VIEW statement 78 | func (p *Parser) parseDropViewStatement() *DropViewStatement { 79 | // Create a new DROP VIEW statement with the DROP token 80 | stmt := &DropViewStatement{ 81 | Token: p.curToken, 82 | } 83 | 84 | // Check for optional IF EXISTS 85 | if p.peekTokenIsKeyword("IF") { 86 | p.nextToken() // Consume IF 87 | 88 | if !p.expectKeyword("EXISTS") { 89 | return nil 90 | } 91 | 92 | stmt.IfExists = true 93 | } 94 | 95 | // Parse view name 96 | if !p.expectPeek(TokenIdentifier) { 97 | p.addError(fmt.Sprintf("expected identifier as view name, got %s at %s", p.peekToken.Literal, p.peekToken.Position)) 98 | return nil 99 | } 100 | 101 | // Create view name identifier 102 | stmt.ViewName = &Identifier{ 103 | Token: p.curToken, 104 | Value: p.curToken.Literal, 105 | } 106 | 107 | return stmt 108 | } 109 | -------------------------------------------------------------------------------- /internal/functions/scalar/ceiling.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "math" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/contract" 23 | "github.com/stoolap/stoolap-go/internal/functions/registry" 24 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 25 | ) 26 | 27 | // CeilingFunction implements the CEILING/CEIL function 28 | type CeilingFunction struct{} 29 | 30 | // Name returns the name of the function 31 | func (f *CeilingFunction) Name() string { 32 | return "CEILING" 33 | } 34 | 35 | // GetInfo returns the function information 36 | func (f *CeilingFunction) GetInfo() funcregistry.FunctionInfo { 37 | return funcregistry.FunctionInfo{ 38 | Name: "CEILING", 39 | Type: funcregistry.ScalarFunction, 40 | Description: "Returns the smallest integer value not less than the argument", 41 | Signature: funcregistry.FunctionSignature{ 42 | ReturnType: funcregistry.TypeFloat, 43 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 44 | MinArgs: 1, 45 | MaxArgs: 1, 46 | IsVariadic: false, 47 | }, 48 | } 49 | } 50 | 51 | // Register registers the function with the registry 52 | func (f *CeilingFunction) Register(registry funcregistry.Registry) { 53 | info := f.GetInfo() 54 | registry.MustRegister(info) 55 | 56 | // Register CEIL as an alias for CEILING 57 | aliasInfo := info 58 | aliasInfo.Name = "CEIL" 59 | registry.MustRegister(aliasInfo) 60 | } 61 | 62 | // Evaluate returns the ceiling of a number 63 | func (f *CeilingFunction) Evaluate(args ...interface{}) (interface{}, error) { 64 | if len(args) != 1 { 65 | return nil, fmt.Errorf("CEILING requires exactly 1 argument, got %d", len(args)) 66 | } 67 | 68 | if args[0] == nil { 69 | return nil, nil 70 | } 71 | 72 | num, err := ConvertToFloat64(args[0]) 73 | if err != nil { 74 | return nil, fmt.Errorf("invalid number: %v", err) 75 | } 76 | 77 | return math.Ceil(num), nil 78 | } 79 | 80 | // NewCeilingFunction creates a new CEILING function 81 | func NewCeilingFunction() contract.ScalarFunction { 82 | return &CeilingFunction{} 83 | } 84 | 85 | // Self-registration 86 | func init() { 87 | // Register the CEILING function with the global registry 88 | // This happens automatically when the package is imported 89 | if registry := registry.GetGlobal(); registry != nil { 90 | registry.RegisterScalarFunction(NewCeilingFunction()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/parser/errors_suggestions_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package parser 17 | 18 | import ( 19 | "strings" 20 | "testing" 21 | ) 22 | 23 | func TestErrorSuggestions(t *testing.T) { 24 | testCases := []struct { 25 | name string 26 | sql string 27 | shouldMatch []string 28 | skip bool // Skip this test case 29 | }{ 30 | { 31 | name: "missing table name", 32 | sql: "SELECT * FROM", 33 | shouldMatch: []string{ 34 | "missing a column or table name", 35 | "reserved keyword", 36 | }, 37 | }, 38 | { 39 | name: "typo in SELECT", 40 | sql: "SELET * FROM users", 41 | shouldMatch: []string{ 42 | "Did you mean 'SELECT'?", 43 | }, 44 | }, 45 | { 46 | name: "missing closing parenthesis", 47 | sql: "SELECT * FROM users WHERE id IN (1, 2, 3", 48 | shouldMatch: []string{ 49 | "closing parenthesis", 50 | "opening parentheses are matched", 51 | }, 52 | }, 53 | { 54 | name: "incorrect JOIN syntax", 55 | sql: "SELECT * FROM users LEFTJOIN orders ON users.id = orders.user_id", 56 | shouldMatch: []string{ 57 | "Did you mean 'LEFT JOIN'?", 58 | "needs a space between", 59 | }, 60 | }, 61 | { 62 | name: "PRAGMA syntax", 63 | sql: "PRAGMA", 64 | shouldMatch: []string{ 65 | "PRAGMA statements follow the format", 66 | "setting_name", 67 | }, 68 | }, 69 | { 70 | name: "missing ON in JOIN", 71 | sql: "SELECT * FROM users JOIN orders", 72 | shouldMatch: []string{ 73 | "JOIN clause is missing the ON condition", 74 | }, 75 | }, 76 | } 77 | 78 | for _, tc := range testCases { 79 | t.Run(tc.name, func(t *testing.T) { 80 | // Parse the SQL and expect errors 81 | result, err := ParseSQL(tc.sql) 82 | 83 | // We expect an error for these test cases 84 | if err == nil { 85 | t.Fatalf("Expected parsing error for '%s', but got success with: %v", tc.sql, result) 86 | } 87 | 88 | // Get formatted error messages 89 | errors := err.(*ParseErrors).Errors 90 | formattedErrors := FormatErrors(tc.sql, errors) 91 | 92 | // Check if error suggestions contain expected text 93 | for _, match := range tc.shouldMatch { 94 | if !strings.Contains(formattedErrors, match) { 95 | t.Errorf("Expected error suggestion to contain '%s', but got:\n%s", match, formattedErrors) 96 | } 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/cte_registry_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go" 23 | ) 24 | 25 | func TestCTERegistry(t *testing.T) { 26 | db, err := stoolap.Open("memory://") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer db.Close() 31 | 32 | ctx := context.Background() 33 | 34 | // Create test table 35 | _, err = db.Exec(ctx, ` 36 | CREATE TABLE test ( 37 | id INTEGER, 38 | value INTEGER 39 | ) 40 | `) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | // Insert test data 46 | _, err = db.Exec(ctx, ` 47 | INSERT INTO test (id, value) VALUES 48 | (1, 100), 49 | (2, 200), 50 | (3, 300) 51 | `) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | // Test: Try to understand what's happening with the CTE registry 57 | // Let's create a simple case that should work 58 | query := ` 59 | WITH cte AS ( 60 | SELECT * FROM test WHERE value >= 200 61 | ) 62 | SELECT id, value, 63 | (SELECT COUNT(*) FROM cte) as cte_count, 64 | (SELECT AVG(value) FROM cte) as cte_avg 65 | FROM cte 66 | ` 67 | 68 | rows, err := db.Query(ctx, query) 69 | if err != nil { 70 | t.Fatalf("Query failed: %v", err) 71 | } 72 | defer rows.Close() 73 | 74 | t.Log("CTE with subqueries in SELECT:") 75 | for rows.Next() { 76 | var id, value, cteCount int 77 | var cteAvg float64 78 | err := rows.Scan(&id, &value, &cteCount, &cteAvg) 79 | if err != nil { 80 | t.Fatalf("Scan failed: %v", err) 81 | } 82 | t.Logf(" id=%d, value=%d, cte_count=%d, cte_avg=%f", id, value, cteCount, cteAvg) 83 | } 84 | 85 | // Now test the problematic case 86 | query2 := ` 87 | WITH cte AS ( 88 | SELECT * FROM test WHERE value >= 200 89 | ) 90 | SELECT id, value 91 | FROM cte 92 | WHERE value > (SELECT AVG(value) FROM cte) 93 | ` 94 | 95 | rows2, err := db.Query(ctx, query2) 96 | if err != nil { 97 | t.Fatalf("Query 2 failed: %v", err) 98 | } 99 | defer rows2.Close() 100 | 101 | t.Log("\nCTE with subquery in WHERE:") 102 | count := 0 103 | for rows2.Next() { 104 | var id, value int 105 | err := rows2.Scan(&id, &value) 106 | if err != nil { 107 | t.Fatalf("Scan failed: %v", err) 108 | } 109 | t.Logf(" id=%d, value=%d", id, value) 110 | count++ 111 | } 112 | t.Logf("Total rows: %d (expecting 1, since only 300 > 250)", count) 113 | } 114 | -------------------------------------------------------------------------------- /test/is_null_simple_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/storage" 22 | "github.com/stoolap/stoolap-go/internal/storage/expression" 23 | ) 24 | 25 | func TestIsNullDirectExpression(t *testing.T) { 26 | // Create a test schema 27 | schema := storage.Schema{ 28 | TableName: "test_table", 29 | Columns: []storage.SchemaColumn{ 30 | {Name: "id", Type: storage.INTEGER}, 31 | {Name: "name", Type: storage.TEXT}, 32 | {Name: "optional_value", Type: storage.INTEGER}, 33 | }, 34 | } 35 | 36 | // Create a test row with NULL value 37 | nullRow := []storage.ColumnValue{ 38 | storage.NewIntegerValue(1), 39 | storage.NewStringValue("Alice"), 40 | storage.NewNullValue(storage.INTEGER), 41 | } 42 | 43 | // Create a test row with non-NULL value 44 | nonNullRow := []storage.ColumnValue{ 45 | storage.NewIntegerValue(2), 46 | storage.NewStringValue("Bob"), 47 | storage.NewIntegerValue(42), 48 | } 49 | 50 | // Test IS NULL on null value 51 | isNullExpr := expression.NewIsNullExpression("optional_value") 52 | isNullExpr = isNullExpr.PrepareForSchema(schema) 53 | 54 | result, err := isNullExpr.Evaluate(nullRow) 55 | if err != nil { 56 | t.Fatalf("Error evaluating IS NULL on null row: %v", err) 57 | } 58 | 59 | if !result { 60 | t.Errorf("Expected nullRow.optional_value IS NULL to be true, got false") 61 | } 62 | 63 | // Test IS NULL on non-null value 64 | result, err = isNullExpr.Evaluate(nonNullRow) 65 | if err != nil { 66 | t.Fatalf("Error evaluating IS NULL on non-null row: %v", err) 67 | } 68 | 69 | if result { 70 | t.Errorf("Expected nonNullRow.optional_value IS NULL to be false, got true") 71 | } 72 | 73 | // Test IS NOT NULL on null value 74 | isNotNullExpr := expression.NewIsNotNullExpression("optional_value") 75 | isNotNullExpr = isNotNullExpr.PrepareForSchema(schema) 76 | 77 | result, err = isNotNullExpr.Evaluate(nullRow) 78 | if err != nil { 79 | t.Fatalf("Error evaluating IS NOT NULL on null row: %v", err) 80 | } 81 | 82 | if result { 83 | t.Errorf("Expected nullRow.optional_value IS NOT NULL to be false, got true") 84 | } 85 | 86 | // Test IS NOT NULL on non-null value 87 | result, err = isNotNullExpr.Evaluate(nonNullRow) 88 | if err != nil { 89 | t.Fatalf("Error evaluating IS NOT NULL on non-null row: %v", err) 90 | } 91 | 92 | if !result { 93 | t.Errorf("Expected nonNullRow.optional_value IS NOT NULL to be true, got false") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/storage/bitmap/like_pattern.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package bitmap 17 | 18 | import "strings" 19 | 20 | // MatchSQLPattern checks if a string matches an SQL LIKE pattern 21 | // Pattern can contain: 22 | // - '%' to match any sequence of characters (including empty) 23 | // - '_' to match exactly one character 24 | func MatchSQLPattern(str, pattern string) bool { 25 | // Standard implementation using the backtracking method 26 | // which is a common algorithm for SQL LIKE pattern matching 27 | 28 | // Case-insensitive comparison (SQL standard) 29 | strLower := strings.ToLower(str) 30 | patternLower := strings.ToLower(pattern) 31 | 32 | // Use an iterative backtracking approach that's more efficient 33 | // for SQL pattern matching and works correctly with wildcard combinations 34 | return matchPattern(strLower, patternLower) 35 | } 36 | 37 | // matchPattern implements the actual pattern matching algorithm 38 | // using an iterative approach with backtracking for SQL LIKE patterns 39 | func matchPattern(str, pattern string) bool { 40 | // Special cases 41 | if pattern == "" { 42 | return str == "" 43 | } 44 | 45 | // Start indices for backtracking 46 | var sp, pp int = 0, 0 47 | 48 | // Indices for potential star match 49 | var starIdx, strBacktrack int = -1, -1 50 | 51 | // Iterate through the string 52 | for sp < len(str) { 53 | // If we have more pattern to match and the current chars match 54 | // (either direct match or '_' wildcard) 55 | if pp < len(pattern) && (pattern[pp] == '_' || pattern[pp] == str[sp]) { 56 | // Move to next char in both strings 57 | sp++ 58 | pp++ 59 | } else if pp < len(pattern) && pattern[pp] == '%' { 60 | // Found a '%' wildcard - mark position for backtracking 61 | starIdx = pp 62 | strBacktrack = sp 63 | 64 | // Move to next pattern char (% can match 0 chars) 65 | pp++ 66 | } else if starIdx != -1 { 67 | // No direct match, but we have a previous '%' to backtrack to 68 | // Reset pattern position to just after the star 69 | pp = starIdx + 1 70 | 71 | // Move string position forward by 1 from last backtrack point 72 | // (% matches one more char) 73 | strBacktrack++ 74 | sp = strBacktrack 75 | } else { 76 | // No match and no backtracking point 77 | return false 78 | } 79 | } 80 | 81 | // Skip any remaining '%' wildcards in pattern 82 | for pp < len(pattern) && pattern[pp] == '%' { 83 | pp++ 84 | } 85 | 86 | // Match if we've used the entire pattern 87 | return pp == len(pattern) 88 | } 89 | -------------------------------------------------------------------------------- /internal/storage/binser/json_encoder.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Package binser provides high-performance binary serialization for the columnar storage engine. 17 | package binser 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "strconv" 24 | "strings" 25 | "sync" 26 | ) 27 | 28 | // JSONBuffer pool for efficient JSON encoding 29 | var jsonBufferPool = sync.Pool{ 30 | New: func() interface{} { 31 | return new(bytes.Buffer) 32 | }, 33 | } 34 | 35 | // GetJSONBuffer gets a buffer from the pool 36 | func GetJSONBuffer() *bytes.Buffer { 37 | buf := jsonBufferPool.Get().(*bytes.Buffer) 38 | buf.Reset() 39 | return buf 40 | } 41 | 42 | // PutJSONBuffer returns a buffer to the pool 43 | func PutJSONBuffer(buf *bytes.Buffer) { 44 | jsonBufferPool.Put(buf) 45 | } 46 | 47 | // ValidateJSON checks if a string contains valid JSON 48 | func ValidateJSON(s string) bool { 49 | if len(s) == 0 { 50 | return false 51 | } 52 | 53 | // Trim whitespace 54 | s = strings.TrimSpace(s) 55 | 56 | // Check for basic structure 57 | if s[0] == '{' && s[len(s)-1] == '}' { // Object 58 | return true 59 | } 60 | if s[0] == '[' && s[len(s)-1] == ']' { // Array 61 | return true 62 | } 63 | if s[0] == '"' && s[len(s)-1] == '"' { // String 64 | return true 65 | } 66 | if s == "null" { // null 67 | return true 68 | } 69 | if s == "true" || s == "false" { // Boolean 70 | return true 71 | } 72 | 73 | // Check if numeric 74 | _, err := strconv.ParseFloat(s, 64) 75 | return err == nil 76 | } 77 | 78 | // EncodeJSON encodes a value to JSON and writes it to the buffer 79 | func EncodeJSON(buf *bytes.Buffer, v interface{}) error { 80 | // This is a future-proof design that allows us to replace the implementation 81 | // with a more efficient one later without changing the API 82 | data, err := json.Marshal(v) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | _, err = buf.Write(data) 88 | return err 89 | } 90 | 91 | // DecodeJSON decodes JSON from a buffer 92 | func DecodeJSON(data []byte, v interface{}) error { 93 | return json.Unmarshal(data, v) 94 | } 95 | 96 | // JSONString encodes a value to a JSON string 97 | func JSONString(v interface{}) (string, error) { 98 | buf := GetJSONBuffer() 99 | defer PutJSONBuffer(buf) 100 | 101 | if err := EncodeJSON(buf, v); err != nil { 102 | return "", fmt.Errorf("JSON encoding error: %w", err) 103 | } 104 | 105 | return buf.String(), nil 106 | } 107 | -------------------------------------------------------------------------------- /test/column_alias_select_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" // Import for database registration 23 | ) 24 | 25 | func TestColumnAliasInSelect(t *testing.T) { 26 | // Open an in-memory database 27 | db, err := sql.Open("stoolap", "memory://") 28 | if err != nil { 29 | t.Fatalf("Error opening database: %v", err) 30 | } 31 | defer db.Close() 32 | 33 | // Create a test table 34 | _, err = db.Exec(`CREATE TABLE items (id INTEGER, price INTEGER, name TEXT)`) 35 | if err != nil { 36 | t.Fatalf("Error creating table: %v", err) 37 | } 38 | 39 | // Insert test data 40 | _, err = db.Exec(`INSERT INTO items (id, price, name) VALUES (1, 100, 'Item A')`) 41 | if err != nil { 42 | t.Fatalf("Error inserting data: %v", err) 43 | } 44 | 45 | // Test simple SELECT with column alias 46 | t.Log("Executing: SELECT price AS cost FROM items") 47 | rows, err := db.Query(`SELECT price FROM items`) 48 | if err != nil { 49 | t.Fatalf("Error selecting without alias: %v", err) 50 | } 51 | 52 | // See if a regular select works 53 | cols, _ := rows.Columns() 54 | t.Logf("Regular SELECT columns: %v", cols) 55 | rows.Close() 56 | 57 | // Now try with an alias - use a query string variable for tracing 58 | aliasQuery := "SELECT price AS cost FROM items" 59 | t.Logf("Now trying with alias: %s", aliasQuery) 60 | rows, err = db.Query(aliasQuery) 61 | if err != nil { 62 | t.Logf("Error details: %+v", err) 63 | t.Fatalf("Error executing SELECT with alias: %v", err) 64 | } 65 | defer rows.Close() 66 | 67 | // Debug: print column information as received 68 | t.Log("Debug: Successfully executed query with alias") 69 | 70 | // Get column names from result set 71 | columns, err := rows.Columns() 72 | if err != nil { 73 | t.Fatalf("Error getting column names: %v", err) 74 | } 75 | 76 | // Verify that the column is named "cost", not "price" 77 | if len(columns) != 1 { 78 | t.Errorf("Expected 1 column, got %d", len(columns)) 79 | } else if columns[0] != "cost" { 80 | t.Errorf("Expected column name to be 'cost', got '%s'", columns[0]) 81 | } 82 | 83 | // Read the row and verify the value 84 | var cost int 85 | if rows.Next() { 86 | err := rows.Scan(&cost) 87 | if err != nil { 88 | t.Fatalf("Error scanning row: %v", err) 89 | } 90 | 91 | if cost != 100 { 92 | t.Errorf("Expected cost = 100, got %d", cost) 93 | } 94 | } else { 95 | t.Errorf("Expected 1 row, got none") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /cmd/stoolap/go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 2 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 3 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 4 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 5 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 6 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 11 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 12 | github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= 13 | github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= 14 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 15 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 19 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 20 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 21 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 22 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 23 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 24 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 25 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 26 | github.com/stoolap/stoolap v0.1.1 h1:iNBhvd9pGL62M+9HIkdyXZIYnBMqv9VkD13t0RwdW7U= 27 | github.com/stoolap/stoolap v0.1.1/go.mod h1:MKu+ADLslhyAFMVzxb29dznlykALBFyZuMpBpNxRAQA= 28 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 29 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 30 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 32 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 33 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /internal/storage/compression/bitpack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package compression 17 | 18 | import ( 19 | "encoding/binary" 20 | "errors" 21 | ) 22 | 23 | // BitPackEncoder implements bit packing for boolean values 24 | // This allows storing 8 boolean values in a single byte 25 | type BitPackEncoder struct{} 26 | 27 | // NewBitPackEncoder creates a new bit pack encoder 28 | func NewBitPackEncoder() *BitPackEncoder { 29 | return &BitPackEncoder{} 30 | } 31 | 32 | // Encode takes a slice of boolean values and packs them into bytes 33 | // where each boolean uses only 1 bit instead of a full byte 34 | func (e *BitPackEncoder) Encode(data []bool) ([]byte, error) { 35 | if len(data) == 0 { 36 | return []byte{}, nil 37 | } 38 | 39 | // Calculate the number of bytes needed to store all bits 40 | // We need (len(data) + 7) / 8 bytes to store len(data) bits 41 | bytesNeeded := (len(data) + 7) / 8 42 | 43 | // Allocate result buffer with additional space for the length 44 | resultBuffer := make([]byte, 4+bytesNeeded) 45 | 46 | // Store the original length at the beginning (4 bytes) 47 | binary.LittleEndian.PutUint32(resultBuffer[0:4], uint32(len(data))) 48 | 49 | // Pack bits into bytes 50 | for i := 0; i < len(data); i++ { 51 | if data[i] { 52 | // Calculate which byte and bit position this boolean value maps to 53 | bytePos := 4 + (i / 8) 54 | bitPos := i % 8 55 | 56 | // Set the corresponding bit 57 | // First, get the current byte value 58 | byteVal := resultBuffer[bytePos] 59 | 60 | // Set the bit at position bitPos 61 | // (using 1 << bitPos to create a byte with only the bit at position bitPos set) 62 | resultBuffer[bytePos] = byteVal | (1 << bitPos) 63 | } 64 | } 65 | 66 | return resultBuffer, nil 67 | } 68 | 69 | // Decode takes bit-packed data and unpacks it into boolean values 70 | func (e *BitPackEncoder) Decode(data []byte) ([]bool, error) { 71 | if len(data) < 4 { 72 | return []bool{}, errors.New("invalid data: too short for BitPack encoding") 73 | } 74 | 75 | // Read the original length 76 | originalLength := binary.LittleEndian.Uint32(data[0:4]) 77 | 78 | // Create result slice with the original length 79 | result := make([]bool, originalLength) 80 | 81 | // Extract each bit 82 | for i := uint32(0); i < originalLength; i++ { 83 | bytePos := 4 + (i / 8) 84 | bitPos := i % 8 85 | 86 | // If we've run out of bytes, fill remaining with false 87 | if int(bytePos) >= len(data) { 88 | break 89 | } 90 | 91 | // Check if the bit at position bitPos is set 92 | result[i] = (data[bytePos] & (1 << bitPos)) != 0 93 | } 94 | 95 | return result, nil 96 | } 97 | -------------------------------------------------------------------------------- /test/sql_literals_simple_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "fmt" 21 | "testing" 22 | 23 | _ "github.com/stoolap/stoolap-go/pkg/driver" // Import for database registration 24 | ) 25 | 26 | func TestSQLLiteralsSimple(t *testing.T) { 27 | // Open an in-memory database 28 | db, err := sql.Open("stoolap", "memory://") 29 | if err != nil { 30 | t.Fatalf("Error opening database: %v", err) 31 | } 32 | defer db.Close() 33 | 34 | // Create a simple test table 35 | _, err = db.Exec(`CREATE TABLE test (id INTEGER, value INTEGER)`) 36 | if err != nil { 37 | t.Fatalf("Error creating table: %v", err) 38 | } 39 | 40 | // Insert test data 41 | _, err = db.Exec(`INSERT INTO test (id, value) VALUES (1, 100)`) 42 | if err != nil { 43 | t.Fatalf("Error inserting data: %v", err) 44 | } 45 | 46 | // Basic tests 47 | tests := []struct { 48 | name string 49 | query string 50 | }{ 51 | { 52 | name: "Simple literal", 53 | query: "SELECT 1", 54 | }, 55 | { 56 | name: "Simple column", 57 | query: "SELECT id FROM test", 58 | }, 59 | { 60 | name: "Simple function", 61 | query: "SELECT UPPER('hello')", 62 | }, 63 | { 64 | name: "Simple alias", 65 | query: "SELECT id AS identifier FROM test", 66 | }, 67 | { 68 | name: "Simple calculation", 69 | query: "SELECT 1 + 2", 70 | }, 71 | { 72 | name: "NOW function", 73 | query: "SELECT NOW()", 74 | }, 75 | } 76 | 77 | for i, test := range tests { 78 | t.Run(fmt.Sprintf("%02d_%s", i+1, test.name), func(t *testing.T) { 79 | t.Logf("Query: %s", test.query) 80 | 81 | rows, err := db.Query(test.query) 82 | if err != nil { 83 | t.Fatalf("Error executing query: %v", err) 84 | } 85 | defer rows.Close() 86 | 87 | columns, err := rows.Columns() 88 | if err != nil { 89 | t.Fatalf("Error getting columns: %v", err) 90 | } 91 | t.Logf("Columns: %v", columns) 92 | 93 | if !rows.Next() { 94 | t.Fatal("Expected a row but got none") 95 | } 96 | 97 | // Dynamically create scanners for the values 98 | values := make([]interface{}, len(columns)) 99 | scanargs := make([]interface{}, len(columns)) 100 | for i := range values { 101 | scanargs[i] = &values[i] 102 | } 103 | 104 | if err := rows.Scan(scanargs...); err != nil { 105 | t.Fatalf("Error scanning row: %v", err) 106 | } 107 | 108 | // Print the values 109 | for i, v := range values { 110 | t.Logf("Column %d (%s): %v (type: %T)", i, columns[i], v, v) 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/cast_direct_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/functions/scalar" 22 | ) 23 | 24 | func TestCastDirect(t *testing.T) { 25 | // Create a CAST function directly 26 | castFunc := scalar.NewCastFunction() 27 | 28 | // Test cases 29 | tests := []struct { 30 | name string 31 | value interface{} 32 | typeName string 33 | expected interface{} 34 | }{ 35 | { 36 | name: "Cast string to integer", 37 | value: "123", 38 | typeName: "INTEGER", 39 | expected: int64(123), 40 | }, 41 | { 42 | name: "Cast float to integer", 43 | value: float64(123.456), 44 | typeName: "INTEGER", 45 | expected: int64(123), 46 | }, 47 | { 48 | name: "Cast bool to integer", 49 | value: true, 50 | typeName: "INTEGER", 51 | expected: int64(1), 52 | }, 53 | { 54 | name: "Cast string to float", 55 | value: "123.456", 56 | typeName: "FLOAT", 57 | expected: float64(123.456), 58 | }, 59 | { 60 | name: "Cast integer to string", 61 | value: int64(123), 62 | typeName: "TEXT", 63 | expected: "123", 64 | }, 65 | { 66 | name: "Cast nil to string", 67 | value: nil, 68 | typeName: "TEXT", 69 | expected: "", 70 | }, 71 | } 72 | 73 | // Run tests 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | // Evaluate CAST function 77 | result, err := castFunc.Evaluate(tt.value, tt.typeName) 78 | if err != nil { 79 | t.Fatalf("CAST evaluation failed: %v", err) 80 | } 81 | 82 | // Check result 83 | t.Logf("Result: %v (type: %T)", result, result) 84 | 85 | // Compare with expected 86 | switch expected := tt.expected.(type) { 87 | case int64: 88 | if value, ok := result.(int64); !ok { 89 | t.Errorf("Expected int64, got %T", result) 90 | } else if value != expected { 91 | t.Errorf("Expected %d, got %d", expected, value) 92 | } 93 | case float64: 94 | if value, ok := result.(float64); !ok { 95 | t.Errorf("Expected float64, got %T", result) 96 | } else if value != expected { 97 | t.Errorf("Expected %f, got %f", expected, value) 98 | } 99 | case string: 100 | if value, ok := result.(string); !ok { 101 | t.Errorf("Expected string, got %T", result) 102 | } else if value != expected { 103 | t.Errorf("Expected %q, got %q", expected, value) 104 | } 105 | default: 106 | t.Errorf("Unexpected type for expected value: %T", tt.expected) 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/functions/aggregate/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package aggregate 17 | 18 | import ( 19 | "sort" 20 | "time" 21 | ) 22 | 23 | type Int64Convertible interface { 24 | AsInt64() (int64, bool) 25 | } 26 | 27 | type Float64Convertible interface { 28 | AsFloat64() (float64, bool) 29 | } 30 | 31 | type BooleanConvertible interface { 32 | AsBoolean() (bool, bool) 33 | } 34 | 35 | type StringConvertible interface { 36 | AsString() (string, bool) 37 | } 38 | 39 | type TimestampConvertible interface { 40 | AsTimestamp() (time.Time, bool) 41 | } 42 | 43 | // DeepCopy creates a copy of simple values to avoid reference sharing 44 | func DeepCopy(val interface{}) interface{} { 45 | if val == nil { 46 | return nil 47 | } 48 | 49 | switch v := val.(type) { 50 | case int: 51 | return v 52 | case int64: 53 | return v 54 | case float64: 55 | return v 56 | case string: 57 | return v 58 | case bool: 59 | return v 60 | default: 61 | // For complex types, we'd need more sophisticated copying 62 | // but for simple database types this should be sufficient 63 | return v 64 | } 65 | } 66 | 67 | // sortOrderedValues sorts a slice of ordered values 68 | func sortOrderedValues(values []struct{ Value, OrderKey interface{} }, descending bool) { 69 | // Sort the values by their order keys 70 | sort.SliceStable(values, func(i, j int) bool { 71 | a, b := values[i].OrderKey, values[j].OrderKey 72 | 73 | // Handle nil values 74 | if a == nil && b == nil { 75 | return false // Equal, preserve original order 76 | } 77 | if a == nil { 78 | return !descending // nil is less than non-nil by default 79 | } 80 | if b == nil { 81 | return descending // non-nil is greater than nil by default 82 | } 83 | 84 | // Compare values based on type 85 | switch av := a.(type) { 86 | case int64: 87 | if bv, ok := b.(int64); ok { 88 | if descending { 89 | return av > bv 90 | } 91 | return av < bv 92 | } 93 | case float64: 94 | if bv, ok := b.(float64); ok { 95 | if descending { 96 | return av > bv 97 | } 98 | return av < bv 99 | } 100 | case string: 101 | if bv, ok := b.(string); ok { 102 | if descending { 103 | return av > bv 104 | } 105 | return av < bv 106 | } 107 | case bool: 108 | if bv, ok := b.(bool); ok { 109 | if descending { 110 | return av && !bv // true > false when descending 111 | } 112 | return !av && bv // false < true when ascending 113 | } 114 | } 115 | 116 | // Default for incomparable types - preserve original order 117 | return false 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /test/update_parentheses_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" // Import the Stoolap driver 23 | ) 24 | 25 | func TestUpdateWithParentheses(t *testing.T) { 26 | db, err := sql.Open("stoolap", "memory://") 27 | if err != nil { 28 | t.Fatalf("Failed to open database: %v", err) 29 | } 30 | defer db.Close() 31 | 32 | // Create and populate test table 33 | _, err = db.Exec(`CREATE TABLE paren_test (id INTEGER PRIMARY KEY, value INTEGER)`) 34 | if err != nil { 35 | t.Fatalf("Failed to create table: %v", err) 36 | } 37 | 38 | _, err = db.Exec("INSERT INTO paren_test (id, value) VALUES (1, 10)") 39 | if err != nil { 40 | t.Fatalf("Failed to insert data: %v", err) 41 | } 42 | 43 | tests := []struct { 44 | name string 45 | query string 46 | expected int 47 | }{ 48 | { 49 | name: "Without parentheses (should fail)", 50 | query: "UPDATE paren_test SET value = value * 2 + 5 WHERE id = 1", 51 | expected: 25, // (10 * 2) + 5 52 | }, 53 | { 54 | name: "With parentheses around entire expression", 55 | query: "UPDATE paren_test SET value = (value * 2 + 5) WHERE id = 1", 56 | expected: 25, // (10 * 2) + 5 57 | }, 58 | { 59 | name: "With parentheses around multiplication", 60 | query: "UPDATE paren_test SET value = (value * 2) + 5 WHERE id = 1", 61 | expected: 25, // (10 * 2) + 5 62 | }, 63 | { 64 | name: "With parentheses around addition", 65 | query: "UPDATE paren_test SET value = value * (2 + 5) WHERE id = 1", 66 | expected: 70, // 10 * (2 + 5) 67 | }, 68 | } 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | // Reset value to 10 before each test 73 | _, err := db.Exec("UPDATE paren_test SET value = 10 WHERE id = 1") 74 | if err != nil { 75 | t.Fatalf("Failed to reset value: %v", err) 76 | } 77 | 78 | // Try the test query 79 | _, err = db.Exec(tt.query) 80 | if err != nil { 81 | t.Logf("❌ Query failed: %s", tt.query) 82 | t.Logf("Error: %v", err) 83 | return 84 | } 85 | 86 | t.Logf("✅ Query succeeded: %s", tt.query) 87 | 88 | // Check the result 89 | var value int 90 | err = db.QueryRow("SELECT value FROM paren_test WHERE id = 1").Scan(&value) 91 | if err != nil { 92 | t.Fatalf("Failed to query result: %v", err) 93 | } 94 | 95 | if value != tt.expected { 96 | t.Errorf("Expected value %d, got %d", tt.expected, value) 97 | } else { 98 | t.Logf("✅ Correct result: %d", value) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/cast_column_alias_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go" 23 | ) 24 | 25 | func TestColumnAlias(t *testing.T) { 26 | ctx := context.Background() 27 | 28 | // Create an in-memory database 29 | db, err := stoolap.Open("memory://") 30 | if err != nil { 31 | t.Fatalf("failed to open database: %v", err) 32 | } 33 | defer db.Close() 34 | 35 | // Get the SQL executor 36 | executor := db.Executor() 37 | 38 | // Create a test table with a simple schema 39 | _, err = executor.Execute(ctx, nil, ` 40 | CREATE TABLE test_alias ( 41 | id INTEGER, 42 | val TEXT 43 | ) 44 | `) 45 | if err != nil { 46 | t.Fatalf("failed to create table: %v", err) 47 | } 48 | 49 | // Insert a single row 50 | _, err = executor.Execute(ctx, nil, ` 51 | INSERT INTO test_alias (id, val) VALUES (1, '123') 52 | `) 53 | if err != nil { 54 | t.Fatalf("failed to insert data: %v", err) 55 | } 56 | 57 | // Test simple column aliases 58 | t.Run("Simple column alias", func(t *testing.T) { 59 | result, err := executor.Execute(ctx, nil, ` 60 | SELECT id AS alias_id, val AS alias_val FROM test_alias 61 | `) 62 | if err != nil { 63 | t.Fatalf("failed to run query: %v", err) 64 | } 65 | 66 | // Print the column names to see what's being returned 67 | t.Logf("Column names: %v", result.Columns()) 68 | 69 | // There should be exactly one row 70 | if !result.Next() { 71 | t.Fatal("expected one row, got none") 72 | } 73 | 74 | var id int64 75 | var val string 76 | if err := result.Scan(&id, &val); err != nil { 77 | t.Fatalf("failed to scan result: %v", err) 78 | } 79 | 80 | t.Logf("Alias result: id=%d, val=%s", id, val) 81 | }) 82 | 83 | // Test expression with alias 84 | t.Run("Expression with alias", func(t *testing.T) { 85 | result, err := executor.Execute(ctx, nil, ` 86 | SELECT (id + 10) AS calculated FROM test_alias 87 | `) 88 | if err != nil { 89 | t.Fatalf("failed to run query: %v", err) 90 | } 91 | 92 | // Print the column names to see what's being returned 93 | t.Logf("Column names: %v", result.Columns()) 94 | 95 | // There should be exactly one row 96 | if !result.Next() { 97 | t.Fatal("expected one row, got none") 98 | } 99 | 100 | var calculated int64 101 | if err := result.Scan(&calculated); err != nil { 102 | t.Fatalf("failed to scan result: %v", err) 103 | } 104 | 105 | t.Logf("Calculated value: %d", calculated) 106 | 107 | // The calculated value should be id + 10 = 1 + 10 = 11 108 | if calculated != 11 { 109 | t.Errorf("expected calculated=11, got %d", calculated) 110 | } 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /test/order_by_parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/parser" 22 | ) 23 | 24 | func TestOrderByInFunctionParsing(t *testing.T) { 25 | // Test cases for SQL statements with ORDER BY inside function calls 26 | testCases := []struct { 27 | name string 28 | sql string 29 | expected bool // Whether parsing should succeed 30 | }{ 31 | { 32 | name: "Simple FIRST with ORDER BY", 33 | sql: "SELECT FIRST(open ORDER BY time_col) FROM candles", 34 | expected: true, 35 | }, 36 | { 37 | name: "FIRST with ORDER BY ASC", 38 | sql: "SELECT FIRST(open ORDER BY time_col ASC) FROM candles", 39 | expected: true, 40 | }, 41 | { 42 | name: "LAST with ORDER BY DESC", 43 | sql: "SELECT LAST(close ORDER BY time_col DESC) FROM candles", 44 | expected: true, 45 | }, 46 | { 47 | name: "Combined MIN, MAX, FIRST, LAST with ORDER BY", 48 | sql: "SELECT FIRST(open ORDER BY time_col), MAX(high), MIN(low), LAST(close ORDER BY time_col), SUM(volume) FROM candles GROUP BY date_col", 49 | expected: true, 50 | }, 51 | // This test case is using non-standard SQL syntax (ORDER BY before FROM), commented out 52 | // { 53 | // name: "COUNT with ORDER BY (should parse even if semantically odd)", 54 | // sql: "SELECT COUNT(*) ORDER BY time_col FROM candles", 55 | // expected: true, 56 | // }, 57 | // Using standard SQL syntax instead 58 | { 59 | name: "COUNT with ORDER BY in standard syntax", 60 | sql: "SELECT COUNT(*) FROM candles ORDER BY time_col", 61 | expected: true, 62 | }, 63 | { 64 | name: "TIME_TRUNC with GROUP BY and ordered aggregates", 65 | sql: "SELECT TIME_TRUNC('15m', event_time) AS bucket, FIRST(price ORDER BY event_time) AS open, MAX(price) AS high, MIN(price) AS low, LAST(price ORDER BY event_time) AS close, SUM(volume) AS volume FROM trades GROUP BY bucket", 66 | expected: true, 67 | }, 68 | } 69 | 70 | for _, tc := range testCases { 71 | t.Run(tc.name, func(t *testing.T) { 72 | // Parse the SQL 73 | stmt, err := parser.Parse(tc.sql) 74 | 75 | if tc.expected { 76 | // Should parse successfully 77 | if err != nil { 78 | t.Fatalf("Expected parsing to succeed, but got error: %v", err) 79 | } 80 | 81 | // Print statement for debugging 82 | t.Logf("Parsed statement: %s", stmt) 83 | 84 | // TODO: Add more specific assertions about the parsed structure, 85 | // checking for the ORDER BY expressions in the function calls 86 | } else { 87 | // Should fail to parse 88 | if err == nil { 89 | t.Fatalf("Expected parsing to fail, but it succeeded: %v", stmt) 90 | } 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/date_format_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | "time" 22 | 23 | _ "github.com/stoolap/stoolap-go/pkg/driver" 24 | ) 25 | 26 | func TestDateFormatting(t *testing.T) { 27 | // Initialize in-memory database 28 | db, err := sql.Open("stoolap", "memory://") 29 | if err != nil { 30 | t.Fatalf("Failed to connect to database: %v", err) 31 | } 32 | defer db.Close() 33 | 34 | // Create a simple test table with date column 35 | _, err = db.Exec(` 36 | CREATE TABLE date_format_test ( 37 | id INTEGER, 38 | date_val DATE 39 | ) 40 | `) 41 | if err != nil { 42 | t.Fatalf("Failed to create table: %v", err) 43 | } 44 | 45 | // Insert test data using SQL 46 | _, err = db.Exec(` 47 | INSERT INTO date_format_test (id, date_val) VALUES 48 | (1, '2023-01-15') 49 | `) 50 | if err != nil { 51 | t.Fatalf("Failed to insert data: %v", err) 52 | } 53 | t.Log("Successfully inserted test data") 54 | 55 | // Verify the row count 56 | var count int 57 | err = db.QueryRow("SELECT COUNT(*) FROM date_format_test").Scan(&count) 58 | if err != nil { 59 | t.Fatalf("Failed to get count: %v", err) 60 | } 61 | t.Logf("Table has %d rows", count) 62 | 63 | // Debug query with interface{} to see the raw type 64 | debugRows, err := db.Query("SELECT * FROM date_format_test") 65 | if err != nil { 66 | t.Fatalf("Failed to debug query all rows: %v", err) 67 | } 68 | 69 | t.Log("DEBUG: All rows with interface{} scan:") 70 | for debugRows.Next() { 71 | var id int 72 | var dateVal interface{} 73 | if err := debugRows.Scan(&id, &dateVal); err != nil { 74 | t.Fatalf("Failed to scan debug row: %v", err) 75 | } 76 | t.Logf("DEBUG: Row data: ID=%d, DATE=%v (type: %T)", id, dateVal, dateVal) 77 | 78 | // If it's a time.Time, format it properly for display 79 | if timeVal, ok := dateVal.(time.Time); ok { 80 | formatted := timeVal.Format("2006-01-02") 81 | t.Logf("DEBUG: Formatted date: %s", formatted) 82 | } 83 | } 84 | debugRows.Close() 85 | 86 | // Now try with a string type scan which is what tests expect 87 | strRows, err := db.Query("SELECT id, date_val FROM date_format_test") 88 | if err != nil { 89 | t.Fatalf("Failed to query rows: %v", err) 90 | } 91 | 92 | t.Log("DEBUG: All rows with string scan:") 93 | for strRows.Next() { 94 | var id int 95 | var dateVal string 96 | if err := strRows.Scan(&id, &dateVal); err != nil { 97 | // If this fails, it means the driver doesn't automatically convert to string 98 | t.Logf("Failed to scan as string: %v", err) 99 | break 100 | } 101 | t.Logf("DEBUG: Row data: ID=%d, DATE=%s", id, dateVal) 102 | } 103 | strRows.Close() 104 | } 105 | -------------------------------------------------------------------------------- /internal/functions/scalar/round.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "math" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/contract" 23 | "github.com/stoolap/stoolap-go/internal/functions/registry" 24 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 25 | ) 26 | 27 | // RoundFunction implements the ROUND function 28 | type RoundFunction struct{} 29 | 30 | // Name returns the name of the function 31 | func (f *RoundFunction) Name() string { 32 | return "ROUND" 33 | } 34 | 35 | // GetInfo returns the function information 36 | func (f *RoundFunction) GetInfo() funcregistry.FunctionInfo { 37 | return funcregistry.FunctionInfo{ 38 | Name: "ROUND", 39 | Type: funcregistry.ScalarFunction, 40 | Description: "Rounds a number to a specified number of decimal places", 41 | Signature: funcregistry.FunctionSignature{ 42 | ReturnType: funcregistry.TypeFloat, 43 | ArgumentTypes: []funcregistry.DataType{ 44 | funcregistry.TypeAny, 45 | funcregistry.TypeAny, 46 | }, 47 | MinArgs: 1, 48 | MaxArgs: 2, 49 | IsVariadic: false, 50 | }, 51 | } 52 | } 53 | 54 | // Register registers the function with the registry 55 | func (f *RoundFunction) Register(registry funcregistry.Registry) { 56 | info := f.GetInfo() 57 | registry.MustRegister(info) 58 | } 59 | 60 | // Evaluate rounds a number 61 | func (f *RoundFunction) Evaluate(args ...interface{}) (interface{}, error) { 62 | if len(args) < 1 || len(args) > 2 { 63 | return nil, fmt.Errorf("ROUND requires 1 or 2 arguments, got %d", len(args)) 64 | } 65 | 66 | if args[0] == nil { 67 | return nil, nil 68 | } 69 | 70 | // Get the number to round 71 | num, err := ConvertToFloat64(args[0]) 72 | if err != nil { 73 | return nil, fmt.Errorf("invalid number to round: %v", err) 74 | } 75 | 76 | // Default to 0 decimal places if not specified 77 | places := 0 78 | 79 | // If decimal places are specified 80 | if len(args) == 2 && args[1] != nil { 81 | p, err := ConvertToInt64(args[1]) 82 | if err != nil { 83 | return nil, fmt.Errorf("invalid decimal places: %v", err) 84 | } 85 | places = int(p) 86 | } 87 | 88 | // Round to specified decimal places 89 | shift := math.Pow(10, float64(places)) 90 | return math.Round(num*shift) / shift, nil 91 | } 92 | 93 | // NewRoundFunction creates a new ROUND function 94 | func NewRoundFunction() contract.ScalarFunction { 95 | return &RoundFunction{} 96 | } 97 | 98 | // Self-registration 99 | func init() { 100 | // Register the ROUND function with the global registry 101 | // This happens automatically when the package is imported 102 | if registry := registry.GetGlobal(); registry != nil { 103 | registry.RegisterScalarFunction(NewRoundFunction()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/storage/mvcc/columnar_index_multi_simple_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package mvcc 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stoolap/stoolap-go/internal/storage" 22 | ) 23 | 24 | func TestEnhancedMultiColumnarIndex_Basic(t *testing.T) { 25 | // Create the original, simple version of MultiColumnarIndex 26 | originalIdx := NewMultiColumnarIndex( 27 | "original_idx", 28 | "test_table", 29 | []string{"id", "name"}, 30 | []int{0, 1}, 31 | []storage.DataType{storage.INTEGER, storage.TEXT}, 32 | nil, 33 | true, // With uniqueness constraint 34 | ) 35 | 36 | // Basic test - add some values 37 | values1 := []storage.ColumnValue{ 38 | storage.NewIntegerValue(1), 39 | storage.NewStringValue("Alice"), 40 | } 41 | if err := originalIdx.Add(values1, 1, 0); err != nil { 42 | t.Fatalf("Failed to add values to original index: %v", err) 43 | } 44 | 45 | values2 := []storage.ColumnValue{ 46 | storage.NewIntegerValue(2), 47 | storage.NewStringValue("Bob"), 48 | } 49 | if err := originalIdx.Add(values2, 2, 0); err != nil { 50 | t.Fatalf("Failed to add values to original index: %v", err) 51 | } 52 | 53 | // Verify columnIndices was initialized correctly 54 | if originalIdx.columnIndices == nil { 55 | t.Fatalf("columnIndices is nil in the enhanced implementation") 56 | } 57 | 58 | if len(originalIdx.columnIndices) != 2 { 59 | t.Fatalf("Expected 2 column indices, got %d", len(originalIdx.columnIndices)) 60 | } 61 | 62 | // Check that individual column indices were created properly 63 | idxId := originalIdx.columnIndices[0] 64 | if idxId == nil { 65 | t.Fatalf("Index for ID column is nil") 66 | } 67 | if idxId.columnName != "id" { 68 | t.Errorf("Expected column name 'id', got '%s'", idxId.columnName) 69 | } 70 | 71 | idxName := originalIdx.columnIndices[1] 72 | if idxName == nil { 73 | t.Fatalf("Index for name column is nil") 74 | } 75 | if idxName.columnName != "name" { 76 | t.Errorf("Expected column name 'name', got '%s'", idxName.columnName) 77 | } 78 | 79 | // Test GetRowIDsEqual with single column 80 | idRows := originalIdx.GetRowIDsEqual([]storage.ColumnValue{storage.NewIntegerValue(1)}) 81 | if len(idRows) != 1 || idRows[0] != 1 { 82 | t.Errorf("Expected to get row ID 1 for ID 1, got %v", idRows) 83 | } 84 | 85 | nameRows := originalIdx.GetRowIDsEqual([]storage.ColumnValue{nil, storage.NewStringValue("Bob")}) 86 | if len(nameRows) != 1 || nameRows[0] != 2 { 87 | t.Errorf("Expected to get row ID 2 for name 'Bob', got %v", nameRows) 88 | } 89 | 90 | // Test GetRowIDsEqual with both columns 91 | bothRows := originalIdx.GetRowIDsEqual([]storage.ColumnValue{ 92 | storage.NewIntegerValue(2), 93 | storage.NewStringValue("Bob"), 94 | }) 95 | if len(bothRows) != 1 || bothRows[0] != 2 { 96 | t.Errorf("Expected to get row ID 2 for ID 2 and name 'Bob', got %v", bothRows) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/functions/aggregate/compare_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package aggregate 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | ) 22 | 23 | type testInt int64 24 | type testFloat float64 25 | 26 | func (m testInt) AsInt64() (int64, bool) { 27 | return int64(m), true 28 | } 29 | 30 | func (m testFloat) AsFloat64() (float64, bool) { 31 | return float64(m), true 32 | } 33 | 34 | func TestIsLessThan(t *testing.T) { 35 | now := time.Now() 36 | later := now.Add(time.Hour) 37 | 38 | tests := []struct { 39 | name string 40 | a, b any 41 | want bool 42 | }{ 43 | // Nil cases 44 | {"nil < int", nil, 1, true}, 45 | {"int < nil", 1, nil, false}, 46 | {"nil < nil", nil, nil, false}, 47 | 48 | // Time comparisons 49 | {"time < time", now, later, true}, 50 | {"time > time", later, now, false}, 51 | {"time < string", now, "abc", true}, 52 | {"string > time", "abc", now, false}, 53 | 54 | // Same type comparisons 55 | {"int < int", 1, 2, true}, 56 | {"int > int", 5, 1, false}, 57 | {"uint < uint", uint(1), uint(2), true}, 58 | {"uint > uint", uint(3), uint(1), false}, 59 | {"float32 < float32", float32(1.1), float32(1.2), true}, 60 | {"float64 < float64", float64(1.1), float64(2.2), true}, 61 | {"bool < bool", false, true, true}, 62 | {"string < string", "abc", "xyz", true}, 63 | 64 | // Cross-type numeric comparisons 65 | {"int32 < uint8", int32(10), uint8(20), true}, 66 | {"uint8 < int32", uint8(5), int32(10), true}, 67 | {"int16 < float64", int16(10), float64(10.5), true}, 68 | {"float32 < int64", float32(1.1), int64(2), true}, 69 | {"int < float32", int(3), float32(4.5), true}, 70 | {"uint < float64", uint(3), float64(3.1), true}, 71 | {"float64 < uint", float64(5.5), uint(6), true}, 72 | {"int64 < uint64", int64(9), uint64(10), true}, 73 | {"int64 < uint64 (negative int)", int64(-5), uint64(1), true}, 74 | {"uint64 < int64", uint64(1), int64(10), true}, 75 | {"uint64 > int64 (negative int)", uint64(10), int64(-1), false}, 76 | 77 | // Equal numeric types (should return false) 78 | {"int == float64", int(3), float64(3.0), false}, 79 | {"uint == int64", uint(100), int64(100), false}, 80 | 81 | // Mixed type comparisons with custom interfaces 82 | {"Int64Convertible < int", testInt(5), 10, true}, 83 | {"Float64Convertible < float64", testFloat(1.5), 2.0, true}, 84 | {"Int64Convertible == int64", testInt(10), int64(10), true}, 85 | {"Float64Convertible == float32", testFloat(3.3), float32(3.3), false}, 86 | 87 | // Fallback type name comparison 88 | {"struct vs int", struct{}{}, 1, typeNameOf(struct{}{}) < typeNameOf(1)}, 89 | } 90 | 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | got := isLessThan(tt.a, tt.b) 94 | if got != tt.want { 95 | t.Errorf("isLessThan(%#v, %#v) = %v, want %v", tt.a, tt.b, got, tt.want) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/update_select_comparison_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "database/sql" 20 | "testing" 21 | 22 | _ "github.com/stoolap/stoolap-go/pkg/driver" // Import the Stoolap driver 23 | ) 24 | 25 | func TestUpdateSelectComparison(t *testing.T) { 26 | db, err := sql.Open("stoolap", "memory://") 27 | if err != nil { 28 | t.Fatalf("Failed to open database: %v", err) 29 | } 30 | defer db.Close() 31 | 32 | // Create test table 33 | _, err = db.Exec(`CREATE TABLE comparison (id INTEGER PRIMARY KEY, original_value INTEGER, updated_value INTEGER, select_value INTEGER)`) 34 | if err != nil { 35 | t.Fatalf("Failed to create table: %v", err) 36 | } 37 | 38 | testValues := []int{10, 20, 5, 0, -3, 100} 39 | 40 | for i, val := range testValues { 41 | id := i + 1 42 | 43 | // Insert original value 44 | _, err = db.Exec("INSERT INTO comparison (id, original_value) VALUES (?, ?)", id, val) 45 | if err != nil { 46 | t.Fatalf("Failed to insert value %d: %v", val, err) 47 | } 48 | 49 | // Calculate what SELECT thinks the result should be 50 | var selectResult int 51 | err = db.QueryRow("SELECT (? * 2 + 5)", val).Scan(&selectResult) 52 | if err != nil { 53 | t.Fatalf("Failed to calculate SELECT result for %d: %v", val, err) 54 | } 55 | 56 | // Store SELECT result 57 | _, err = db.Exec("UPDATE comparison SET select_value = ? WHERE id = ?", selectResult, id) 58 | if err != nil { 59 | t.Fatalf("Failed to store SELECT result: %v", err) 60 | } 61 | 62 | t.Logf("Value %d: SELECT (%d * 2 + 5) = %d", val, val, selectResult) 63 | } 64 | 65 | // Now test UPDATE with arithmetic expressions 66 | _, err = db.Exec("UPDATE comparison SET updated_value = (original_value * 2 + 5)") 67 | if err != nil { 68 | t.Fatalf("Failed to execute UPDATE with arithmetic: %v", err) 69 | } 70 | 71 | // Compare results 72 | t.Log("\nComparison Results:") 73 | rows, err := db.Query("SELECT id, original_value, updated_value, select_value FROM comparison ORDER BY id") 74 | if err != nil { 75 | t.Fatalf("Failed to query comparison results: %v", err) 76 | } 77 | defer rows.Close() 78 | 79 | allMatch := true 80 | for rows.Next() { 81 | var id, original, updated, selectVal int 82 | err := rows.Scan(&id, &original, &updated, &selectVal) 83 | if err != nil { 84 | t.Fatalf("Failed to scan comparison results: %v", err) 85 | } 86 | 87 | match := updated == selectVal 88 | if !match { 89 | allMatch = false 90 | } 91 | 92 | status := "✅" 93 | if !match { 94 | status = "❌" 95 | } 96 | 97 | t.Logf("%s Original: %d, UPDATE result: %d, SELECT result: %d", status, original, updated, selectVal) 98 | } 99 | 100 | if !allMatch { 101 | t.Error("UPDATE and SELECT arithmetic expressions produce different results!") 102 | } else { 103 | t.Log("🎉 All UPDATE and SELECT arithmetic expressions match perfectly!") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/stress_lost_update_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "database/sql/driver" 21 | "fmt" 22 | "sync" 23 | "testing" 24 | 25 | "github.com/stoolap/stoolap-go" 26 | "github.com/stoolap/stoolap-go/internal/storage" 27 | ) 28 | 29 | // TestStressLostUpdate aggressively tests for lost updates 30 | func TestStressLostUpdate(t *testing.T) { 31 | for round := 0; round < 10; round++ { 32 | db, err := stoolap.Open("memory://") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | ctx := context.Background() 38 | err = db.Engine().SetIsolationLevel(storage.SnapshotIsolation) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | // Create table 44 | _, err = db.Exec(ctx, "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)") 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | // Insert initial data 50 | _, err = db.Exec(ctx, "INSERT INTO test (id, value) VALUES (1, 0)") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | // Run 100 concurrent increments 56 | const numWorkers = 100 57 | var wg sync.WaitGroup 58 | successCount := 0 59 | var mu sync.Mutex 60 | 61 | for i := 0; i < numWorkers; i++ { 62 | wg.Add(1) 63 | go func() { 64 | defer wg.Done() 65 | 66 | tx, err := db.Begin() 67 | if err != nil { 68 | return 69 | } 70 | 71 | // Read current value 72 | var currentValue int 73 | rows, err := tx.QueryContext(ctx, "SELECT value FROM test WHERE id = 1") 74 | if err != nil { 75 | tx.Rollback() 76 | return 77 | } 78 | if rows.Next() { 79 | rows.Scan(¤tValue) 80 | rows.Close() 81 | } else { 82 | rows.Close() 83 | tx.Rollback() 84 | return 85 | } 86 | 87 | // Update with increment 88 | _, err = tx.ExecContext(ctx, 89 | "UPDATE test SET value = ? WHERE id = 1", 90 | driver.NamedValue{Ordinal: 1, Value: currentValue + 1}) 91 | if err != nil { 92 | tx.Rollback() 93 | return 94 | } 95 | 96 | // Try to commit 97 | err = tx.Commit() 98 | if err == nil { 99 | mu.Lock() 100 | successCount++ 101 | mu.Unlock() 102 | } 103 | }() 104 | } 105 | 106 | wg.Wait() 107 | 108 | // Check final value 109 | var finalValue int 110 | rows, _ := db.Query(ctx, "SELECT value FROM test WHERE id = 1") 111 | if rows.Next() { 112 | rows.Scan(&finalValue) 113 | rows.Close() 114 | } 115 | 116 | fmt.Printf("Round %d: %d successful commits, final value = %d", 117 | round+1, successCount, finalValue) 118 | 119 | if finalValue != successCount { 120 | fmt.Printf(" - LOST %d UPDATES!\n", successCount-finalValue) 121 | t.Errorf("Round %d: Lost updates detected! Final value %d != successful commits %d", 122 | round+1, finalValue, successCount) 123 | } else { 124 | fmt.Printf(" - OK\n") 125 | } 126 | 127 | db.Close() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/cast_evaluator_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package test 17 | 18 | import ( 19 | "context" 20 | "testing" 21 | 22 | "github.com/stoolap/stoolap-go/internal/functions/registry" 23 | "github.com/stoolap/stoolap-go/internal/parser" 24 | sql "github.com/stoolap/stoolap-go/internal/sql/executor" 25 | ) 26 | 27 | func TestCastEvaluator(t *testing.T) { 28 | // Create an evaluator 29 | evaluator := sql.NewEvaluator(context.Background(), registry.GetGlobal()) 30 | 31 | // Test cases 32 | tests := []struct { 33 | name string 34 | expr parser.Expression 35 | expected interface{} 36 | }{ 37 | { 38 | name: "Cast string to int", 39 | expr: &parser.CastExpression{ 40 | Expr: &parser.StringLiteral{Value: "123"}, 41 | TypeName: "INTEGER", 42 | }, 43 | expected: int64(123), 44 | }, 45 | { 46 | name: "Cast string to float", 47 | expr: &parser.CastExpression{ 48 | Expr: &parser.StringLiteral{Value: "3.14"}, 49 | TypeName: "FLOAT", 50 | }, 51 | expected: float64(3.14), 52 | }, 53 | { 54 | name: "Cast int to string", 55 | expr: &parser.CastExpression{ 56 | Expr: &parser.IntegerLiteral{Value: 42}, 57 | TypeName: "TEXT", 58 | }, 59 | expected: "42", 60 | }, 61 | { 62 | name: "Cast string to boolean", 63 | expr: &parser.CastExpression{ 64 | Expr: &parser.StringLiteral{Value: "true"}, 65 | TypeName: "BOOLEAN", 66 | }, 67 | expected: true, 68 | }, 69 | } 70 | 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | // Evaluate the expression 74 | result, err := evaluator.Evaluate(tt.expr) 75 | if err != nil { 76 | t.Fatalf("Error evaluating expression: %v", err) 77 | } 78 | 79 | t.Logf("Result: %v (type: %T)", result.AsInterface(), result.AsInterface()) 80 | 81 | // Check the result type and value 82 | switch expected := tt.expected.(type) { 83 | case int64: 84 | if val, ok := result.AsInterface().(int64); !ok { 85 | t.Errorf("Expected int64, got %T", result.AsInterface()) 86 | } else if val != expected { 87 | t.Errorf("Expected %d, got %d", expected, val) 88 | } 89 | case float64: 90 | if val, ok := result.AsInterface().(float64); !ok { 91 | t.Errorf("Expected float64, got %T", result.AsInterface()) 92 | } else if val != expected { 93 | t.Errorf("Expected %f, got %f", expected, val) 94 | } 95 | case string: 96 | if val, ok := result.AsInterface().(string); !ok { 97 | t.Errorf("Expected string, got %T", result.AsInterface()) 98 | } else if val != expected { 99 | t.Errorf("Expected %s, got %s", expected, val) 100 | } 101 | case bool: 102 | if val, ok := result.AsInterface().(bool); !ok { 103 | t.Errorf("Expected bool, got %T", result.AsInterface()) 104 | } else if val != expected { 105 | t.Errorf("Expected %v, got %v", expected, val) 106 | } 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/functions/scalar/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package scalar 17 | 18 | import ( 19 | "fmt" 20 | "strconv" 21 | ) 22 | 23 | // ConvertToFloat64 attempts to convert a value to float64 24 | func ConvertToFloat64(value interface{}) (float64, error) { 25 | if value == nil { 26 | return 0, fmt.Errorf("cannot convert nil to float64") 27 | } 28 | 29 | switch v := value.(type) { 30 | case int: 31 | return float64(v), nil 32 | case int64: 33 | return float64(v), nil 34 | case float64: 35 | return v, nil 36 | case string: 37 | f, err := strconv.ParseFloat(v, 64) 38 | if err != nil { 39 | return 0, fmt.Errorf("cannot convert string %q to float64: %v", v, err) 40 | } 41 | return f, nil 42 | case bool: 43 | if v { 44 | return 1, nil 45 | } 46 | return 0, nil 47 | } 48 | 49 | return 0, fmt.Errorf("cannot convert %T to float64", value) 50 | } 51 | 52 | // ConvertToString converts a value to string 53 | func ConvertToString(value interface{}) string { 54 | if value == nil { 55 | return "" 56 | } 57 | 58 | switch v := value.(type) { 59 | case string: 60 | return v 61 | case int, int64, float64, bool: 62 | return fmt.Sprintf("%v", v) 63 | default: 64 | return fmt.Sprintf("%v", v) 65 | } 66 | } 67 | 68 | // ConvertToInt64 attempts to convert a value to int64 69 | func ConvertToInt64(value interface{}) (int64, error) { 70 | if value == nil { 71 | return 0, fmt.Errorf("cannot convert nil to int64") 72 | } 73 | 74 | switch v := value.(type) { 75 | case int: 76 | return int64(v), nil 77 | case int64: 78 | return v, nil 79 | case float64: 80 | return int64(v), nil 81 | case string: 82 | i, err := strconv.ParseInt(v, 10, 64) 83 | if err != nil { 84 | // Try float conversion if integer parsing fails 85 | f, err := strconv.ParseFloat(v, 64) 86 | if err != nil { 87 | return 0, fmt.Errorf("cannot convert string %q to int64: %v", v, err) 88 | } 89 | return int64(f), nil 90 | } 91 | return i, nil 92 | case bool: 93 | if v { 94 | return 1, nil 95 | } 96 | return 0, nil 97 | } 98 | 99 | return 0, fmt.Errorf("cannot convert %T to int64", value) 100 | } 101 | 102 | // ConvertToBool attempts to convert a value to bool 103 | func ConvertToBool(value interface{}) (bool, error) { 104 | if value == nil { 105 | return false, nil 106 | } 107 | 108 | switch v := value.(type) { 109 | case bool: 110 | return v, nil 111 | case int: 112 | return v != 0, nil 113 | case int64: 114 | return v != 0, nil 115 | case float64: 116 | return v != 0, nil 117 | case string: 118 | switch v { 119 | case "1", "true", "TRUE", "True", "T", "t", "yes", "YES", "Yes": 120 | return true, nil 121 | case "0", "false", "FALSE", "False", "F", "f", "no", "NO", "No": 122 | return false, nil 123 | default: 124 | return false, fmt.Errorf("cannot convert string %q to bool", v) 125 | } 126 | } 127 | 128 | return false, fmt.Errorf("cannot convert %T to bool", value) 129 | } 130 | -------------------------------------------------------------------------------- /internal/parser/funcregistry/validator.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package funcregistry 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | // Validator is responsible for validating function calls 24 | // and populating function info in the AST 25 | type Validator struct { 26 | registry Registry 27 | } 28 | 29 | // NewValidator creates a new function validator 30 | func NewValidator(registry Registry) *Validator { 31 | return &Validator{ 32 | registry: registry, 33 | } 34 | } 35 | 36 | // ExpressionType represents the data type of an expression 37 | type ExpressionType interface { 38 | // GetType returns the data type of the expression 39 | GetType() DataType 40 | } 41 | 42 | // ValidateFunctionCall validates a function call 43 | // It populates the FunctionInfo field of the provided function call info 44 | func (v *Validator) ValidateFunctionCall(functionName string, infoPtr **FunctionInfo, argTypes []DataType) error { 45 | // Function names are case-insensitive in SQL 46 | functionName = strings.ToUpper(functionName) 47 | 48 | // Get function information from registry 49 | info, err := v.registry.Get(functionName) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // Validate argument types 55 | if err := info.Signature.ValidateArgs(argTypes); err != nil { 56 | return err 57 | } 58 | 59 | // Update the function info in the caller 60 | *infoPtr = &info 61 | 62 | return nil 63 | } 64 | 65 | // GetKnownDataType tries to determine the data type of a value 66 | func GetKnownDataType(value interface{}) DataType { 67 | switch v := value.(type) { 68 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 69 | return TypeInteger 70 | case float32, float64: 71 | return TypeFloat 72 | case string: 73 | return TypeString 74 | case bool: 75 | return TypeBoolean 76 | case nil: 77 | return TypeUnknown 78 | default: 79 | // Try to get more specific types 80 | typeStr := fmt.Sprintf("%T", v) 81 | 82 | if strings.Contains(typeStr, "Time") || 83 | strings.Contains(typeStr, "Date") { 84 | return TypeDateTime 85 | } 86 | 87 | if strings.Contains(typeStr, "map") || 88 | strings.Contains(typeStr, "struct") { 89 | return TypeJSON 90 | } 91 | 92 | if strings.Contains(typeStr, "slice") || 93 | strings.Contains(typeStr, "array") { 94 | return TypeArray 95 | } 96 | 97 | return TypeUnknown 98 | } 99 | } 100 | 101 | // DetermineLiteralType determines the type of a literal value 102 | // This is a simple implementation; in a real system you would want more nuanced type detection 103 | func DetermineLiteralType(literals map[string]interface{}) map[string]DataType { 104 | result := make(map[string]DataType) 105 | 106 | for name, value := range literals { 107 | result[name] = GetKnownDataType(value) 108 | } 109 | 110 | return result 111 | } 112 | 113 | // TypeOf returns the data type of an expression type 114 | func TypeOf(expr ExpressionType) DataType { 115 | if expr == nil { 116 | return TypeUnknown 117 | } 118 | return expr.GetType() 119 | } 120 | -------------------------------------------------------------------------------- /internal/functions/aggregate/count.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package aggregate 17 | 18 | import ( 19 | "github.com/stoolap/stoolap-go/internal/functions/contract" 20 | "github.com/stoolap/stoolap-go/internal/functions/registry" 21 | "github.com/stoolap/stoolap-go/internal/parser/funcregistry" 22 | ) 23 | 24 | // CountFunction implements the COUNT aggregate function 25 | type CountFunction struct { 26 | count int64 27 | distinct bool 28 | values map[interface{}]struct{} // used for DISTINCT 29 | } 30 | 31 | // Name returns the name of the function 32 | func (f *CountFunction) Name() string { 33 | return "COUNT" 34 | } 35 | 36 | // GetInfo returns the function information 37 | func (f *CountFunction) GetInfo() funcregistry.FunctionInfo { 38 | return funcregistry.FunctionInfo{ 39 | Name: "COUNT", 40 | Type: funcregistry.AggregateFunction, 41 | Description: "Returns the number of rows matching the query criteria", 42 | Signature: funcregistry.FunctionSignature{ 43 | ReturnType: funcregistry.TypeInteger, 44 | ArgumentTypes: []funcregistry.DataType{funcregistry.TypeAny}, 45 | MinArgs: 0, // COUNT(*) has no actual argument 46 | MaxArgs: 1, // But can be COUNT(column) or COUNT(DISTINCT column) 47 | IsVariadic: false, 48 | }, 49 | } 50 | } 51 | 52 | // Register registers the COUNT function with the registry 53 | func (f *CountFunction) Register(registry funcregistry.Registry) { 54 | info := f.GetInfo() 55 | registry.MustRegister(info) 56 | } 57 | 58 | // Accumulate adds a value to the COUNT calculation 59 | func (f *CountFunction) Accumulate(value interface{}, distinct bool) { 60 | f.distinct = distinct 61 | 62 | // Handle NULL values (COUNT ignores NULLs except for COUNT(*)) 63 | if value == nil { 64 | return 65 | } 66 | 67 | // Special case for COUNT(*) which counts rows, not values 68 | if value == "*" { 69 | f.count++ 70 | return 71 | } 72 | 73 | // Handle DISTINCT case 74 | if distinct { 75 | if f.values == nil { 76 | f.values = make(map[interface{}]struct{}) 77 | } 78 | f.values[value] = struct{}{} 79 | } else { 80 | // Regular COUNT 81 | f.count++ 82 | } 83 | } 84 | 85 | // Result returns the final result of the COUNT calculation 86 | func (f *CountFunction) Result() interface{} { 87 | if f.distinct && f.values != nil { 88 | return int64(len(f.values)) 89 | } 90 | return f.count 91 | } 92 | 93 | // Reset resets the COUNT calculation 94 | func (f *CountFunction) Reset() { 95 | f.count = 0 96 | f.values = nil 97 | f.distinct = false 98 | } 99 | 100 | // NewCountFunction creates a new COUNT function 101 | func NewCountFunction() contract.AggregateFunction { 102 | return &CountFunction{ 103 | count: 0, 104 | values: make(map[interface{}]struct{}), 105 | distinct: false, 106 | } 107 | } 108 | 109 | // Self-registration 110 | func init() { 111 | // Register the COUNT function with the global registry 112 | // This happens automatically when the package is imported 113 | if registry := registry.GetGlobal(); registry != nil { 114 | registry.RegisterAggregateFunction(NewCountFunction()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /internal/fastmap/simple_int64_benchmark_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Stoolap Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package fastmap 17 | 18 | import ( 19 | "testing" 20 | ) 21 | 22 | // BenchmarkSimpleMapOperations benchmarks both maps with simple operations 23 | func BenchmarkSimpleMapOperations(b *testing.B) { 24 | // Generate sequential keys 25 | const size = 10000 26 | keys := make([]int64, size) 27 | for i := range keys { 28 | keys[i] = int64(i) 29 | } 30 | 31 | // Benchmark Put operations 32 | b.Run("Put", func(b *testing.B) { 33 | b.Run("Int64Map", func(b *testing.B) { 34 | m := NewInt64Map[int64](size) 35 | b.ReportAllocs() 36 | b.ResetTimer() 37 | 38 | for i := 0; i < b.N; i++ { 39 | key := keys[i%size] 40 | m.Put(key, key) 41 | } 42 | }) 43 | 44 | b.Run("StdMap", func(b *testing.B) { 45 | m := make(map[int64]int64, size) 46 | b.ReportAllocs() 47 | b.ResetTimer() 48 | 49 | for i := 0; i < b.N; i++ { 50 | key := keys[i%size] 51 | m[key] = key 52 | } 53 | }) 54 | }) 55 | 56 | // Benchmark Get operations 57 | b.Run("Get", func(b *testing.B) { 58 | fastMap := NewInt64Map[int64](size) 59 | stdMap := make(map[int64]int64, size) 60 | 61 | // Populate maps 62 | for i := 0; i < size; i++ { 63 | fastMap.Put(keys[i], keys[i]) 64 | stdMap[keys[i]] = keys[i] 65 | } 66 | 67 | b.Run("Int64Map", func(b *testing.B) { 68 | b.ReportAllocs() 69 | b.ResetTimer() 70 | 71 | for i := 0; i < b.N; i++ { 72 | key := keys[i%size] 73 | _, _ = fastMap.Get(key) 74 | } 75 | }) 76 | 77 | b.Run("StdMap", func(b *testing.B) { 78 | b.ReportAllocs() 79 | b.ResetTimer() 80 | 81 | for i := 0; i < b.N; i++ { 82 | key := keys[i%size] 83 | _ = stdMap[key] 84 | } 85 | }) 86 | }) 87 | 88 | // Benchmark Has operation 89 | b.Run("Has", func(b *testing.B) { 90 | fastMap := NewInt64Map[int64](size) 91 | 92 | // Populate maps 93 | for i := 0; i < size; i++ { 94 | fastMap.Put(keys[i], keys[i]) 95 | } 96 | 97 | b.Run("Int64Map", func(b *testing.B) { 98 | b.ReportAllocs() 99 | b.ResetTimer() 100 | 101 | for i := 0; i < b.N; i++ { 102 | key := keys[i%size] 103 | _ = fastMap.Has(key) 104 | } 105 | }) 106 | }) 107 | 108 | // Benchmark Delete operation 109 | b.Run("Delete", func(b *testing.B) { 110 | b.Run("Int64Map", func(b *testing.B) { 111 | // Generate a fresh map for each run 112 | m := NewInt64Map[int64](size) 113 | 114 | // Populate map 115 | for i := 0; i < size; i++ { 116 | m.Put(keys[i], keys[i]) 117 | } 118 | 119 | b.ReportAllocs() 120 | b.ResetTimer() 121 | 122 | for i := 0; i < b.N; i++ { 123 | key := keys[i%size] 124 | m.Del(key) 125 | } 126 | }) 127 | 128 | b.Run("StdMap", func(b *testing.B) { 129 | // Generate a fresh map for each run 130 | m := make(map[int64]int64, size) 131 | 132 | // Populate map 133 | for i := 0; i < size; i++ { 134 | m[keys[i]] = keys[i] 135 | } 136 | 137 | b.ReportAllocs() 138 | b.ResetTimer() 139 | 140 | for i := 0; i < b.N; i++ { 141 | key := keys[i%size] 142 | delete(m, key) 143 | } 144 | }) 145 | }) 146 | } 147 | --------------------------------------------------------------------------------