Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/chrondb/api/sql/protocol/constants.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

;; Message Types
(def PG_ERROR_RESPONSE (byte (int \E)))
(def PG_NOTICE_RESPONSE (byte (int \N))) ;; Added for welcome messages
(def PG_READY_FOR_QUERY (byte (int \Z)))
(def PG_ROW_DESCRIPTION (byte (int \T)))
(def PG_DATA_ROW (byte (int \D)))
Expand Down
3 changes: 3 additions & 0 deletions src/chrondb/api/sql/protocol/handlers.clj
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
(messages/send-parameter-status out "server_version" "9.5.0")
(messages/send-parameter-status out "client_encoding" "UTF8")

;; Send welcome message
(messages/send-notice-response out "Welcome to ChronDB - Git-backed Versioned Database System")

;; Indicate that we're ready to process queries
(messages/send-ready-for-query out \I)

Expand Down
27 changes: 27 additions & 0 deletions src/chrondb/api/sql/protocol/messages.clj
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,33 @@
(.get buffer final-data)
(write-message out constants/PG_ERROR_RESPONSE final-data)))

(defn send-notice-response
"Sends a notice response message to the client.
Parameters:
- out: The output stream
- message: The notice message
Returns: nil"
[^OutputStream out message]
(let [buffer (ByteBuffer/allocate 1024)
;; Add notice fields
_ (.put buffer (byte (int \S))) ;; Severity field
_ (.put buffer (.getBytes "NOTICE" StandardCharsets/UTF_8))
_ (.put buffer (byte 0)) ;; Null terminator

;; Message field
_ (.put buffer (byte (int \M))) ;; Message field
_ (.put buffer (.getBytes message StandardCharsets/UTF_8))
_ (.put buffer (byte 0)) ;; Null terminator

;; Final null terminator for the entire message
_ (.put buffer (byte 0))

pos (.position buffer)
final-data (byte-array pos)]
(.flip buffer)
(.get buffer final-data)
(write-message out constants/PG_NOTICE_RESPONSE final-data)))

(defn send-parameter-status
"Sends a parameter status message to inform client of settings
Parameters:
Expand Down
65 changes: 65 additions & 0 deletions test/chrondb/api/sql/execution/functions_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
(ns chrondb.api.sql.execution.functions-test
(:require [clojure.test :refer [deftest is testing]]
[chrondb.api.sql.execution.functions :as functions]))

(deftest test-process-aggregate-result
(testing "process-aggregate-result with count function"
(is (= {:count_users 5}
(functions/process-aggregate-result :count 5 "users"))))

(testing "process-aggregate-result with sum function"
(is (= {:sum_value 100}
(functions/process-aggregate-result :sum 100 "value"))))

(testing "process-aggregate-result with avg function"
(is (= {:avg_price 25.5}
(functions/process-aggregate-result :avg 25.5 "price"))))

(testing "process-aggregate-result with nil result for count"
(is (= {:count_users 0}
(functions/process-aggregate-result :count nil "users"))))

(testing "process-aggregate-result with nil result for sum"
(is (= {:sum_value 0}
(functions/process-aggregate-result :sum nil "value"))))

(testing "process-aggregate-result with nil result for avg"
(is (= {:avg_price 0}
(functions/process-aggregate-result :avg nil "price")))))

(defn- approximately=
"Compare two numbers for approximate equality within a small delta"
[a b]
(let [delta 0.0001]
(< (Math/abs (- a b)) delta)))

(deftest test-execute-aggregate-function
(let [test-docs [{:id "user:1", :name "Alice", :age 30, :score 85.5}
{:id "user:2", :name "Bob", :age 25, :score 92.0}
{:id "user:3", :name "Charlie", :age 35, :score 78.3}
{:id "user:4", :name "Diana", :age 28, :score 90.1}
{:id "user:5", :name "Eve", :age nil, :score nil}]]

(testing "count function for all documents"
(is (= 5 (functions/execute-aggregate-function :count test-docs "*"))))

(testing "count function for non-null field"
(is (= 4 (functions/execute-aggregate-function :count test-docs "age"))))

(testing "sum function"
(is (= 118 (functions/execute-aggregate-function :sum test-docs "age"))))

(testing "avg function"
(is (approximately= 29.5 (functions/execute-aggregate-function :avg test-docs "age"))))

(testing "min function"
(is (approximately= 25.0 (functions/execute-aggregate-function :min test-docs "age"))))

(testing "max function"
(is (approximately= 35.0 (functions/execute-aggregate-function :max test-docs "age"))))

(testing "numeric extraction from id field"
(is (approximately= 15 (functions/execute-aggregate-function :sum test-docs "id"))))

(testing "unsupported aggregate function"
(is (nil? (functions/execute-aggregate-function :unknown test-docs "age"))))))
137 changes: 137 additions & 0 deletions test/chrondb/api/sql/execution/operators_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
(ns chrondb.api.sql.execution.operators-test
(:require [clojure.test :refer [deftest is testing]]
[chrondb.api.sql.execution.operators :as operators]))

(deftest test-evaluate-condition
(let [doc {:id "user:1"
:name "Alice"
:age 30
:active true}]

(testing "Equal operator"
(is (operators/evaluate-condition doc {:field "name" :op "=" :value "Alice"}))
(is (not (operators/evaluate-condition doc {:field "name" :op "=" :value "Bob"}))))

(testing "Not equal operators"
(is (operators/evaluate-condition doc {:field "name" :op "!=" :value "Bob"}))
(is (not (operators/evaluate-condition doc {:field "name" :op "!=" :value "Alice"})))

(is (operators/evaluate-condition doc {:field "name" :op "<>" :value "Bob"}))
(is (not (operators/evaluate-condition doc {:field "name" :op "<>" :value "Alice"}))))

(testing "Greater than operator"
(is (operators/evaluate-condition doc {:field "age" :op ">" :value "20"}))
(is (not (operators/evaluate-condition doc {:field "age" :op ">" :value "30"}))))

(testing "Less than operator"
(is (operators/evaluate-condition doc {:field "age" :op "<" :value "40"}))
(is (not (operators/evaluate-condition doc {:field "age" :op "<" :value "30"}))))

(testing "Greater than or equal operator"
(is (operators/evaluate-condition doc {:field "age" :op ">=" :value "30"}))
(is (not (operators/evaluate-condition doc {:field "age" :op ">=" :value "31"}))))

(testing "Less than or equal operator"
(is (operators/evaluate-condition doc {:field "age" :op "<=" :value "30"}))
(is (not (operators/evaluate-condition doc {:field "age" :op "<=" :value "29"}))))

(testing "LIKE operator"
(is (operators/evaluate-condition doc {:field "name" :op "like" :value "Al%"}))
(is (operators/evaluate-condition doc {:field "name" :op "like" :value "%ice"}))
(is (operators/evaluate-condition doc {:field "name" :op "like" :value "%lic%"}))
(is (not (operators/evaluate-condition doc {:field "name" :op "like" :value "Bob%"}))))

(testing "Unsupported operator"
(is (not (operators/evaluate-condition doc {:field "name" :op "unknown" :value "Alice"}))))))

(deftest test-apply-where-conditions
(let [docs [{:id "user:1" :name "Alice" :age 30 :active true}
{:id "user:2" :name "Bob" :age 25 :active false}
{:id "user:3" :name "Charlie" :age 35 :active true}
{:id "user:4" :name "Diana" :age 28 :active false}]]

(testing "Empty conditions"
(is (= docs (operators/apply-where-conditions docs []))))

(testing "Single condition"
(let [conditions [{:field "age" :op ">" :value "28"}]]
(is (= 2 (count (operators/apply-where-conditions docs conditions))))
(is (= ["user:1" "user:3"] (map :id (operators/apply-where-conditions docs conditions))))))

(testing "Multiple conditions (AND)"
(let [conditions [{:field "age" :op ">" :value "25"}
{:field "active" :op "=" :value "true"}]]
(is (= 2 (count (operators/apply-where-conditions docs conditions))))
(is (= ["user:1" "user:3"] (map :id (operators/apply-where-conditions docs conditions))))))

(testing "LIKE condition"
(let [conditions [{:field "name" :op "LIKE" :value "D%"}]]
(is (= 1 (count (operators/apply-where-conditions docs conditions))))
(is (= ["user:4"] (map :id (operators/apply-where-conditions docs conditions))))))))

(deftest test-group-docs-by
(let [docs [{:id "user:1" :name "Alice" :dept "IT" :role "Dev"}
{:id "user:2" :name "Bob" :dept "HR" :role "Manager"}
{:id "user:3" :name "Charlie" :dept "IT" :role "Dev"}
{:id "user:4" :name "Diana" :dept "HR" :role "Admin"}]]

(testing "Empty group fields"
(is (= [docs] (operators/group-docs-by docs []))))

(testing "Group by single field"
(let [groups (operators/group-docs-by docs [{:column "dept"}])
expected-count 2] ;; IT and HR groups
(is (= expected-count (count groups)))
;; Each group should have docs with the same dept
(doseq [group groups]
(is (apply = (map :dept group))))))

(testing "Group by multiple fields"
(let [groups (operators/group-docs-by docs [{:column "dept"} {:column "role"}])
expected-count 3] ;; IT+Dev, HR+Manager, HR+Admin
(is (= expected-count (count groups)))))))

(deftest test-sort-docs-by
(let [docs [{:id "user:1" :name "Alice" :age 30 :dept "IT"}
{:id "user:2" :name "Bob" :age 25 :dept "HR"}
{:id "user:3" :name "Charlie" :age 35 :dept "IT"}
{:id "user:4" :name "Diana" :age 28 :dept "HR"}]]

(testing "Empty order clauses"
(is (= docs (operators/sort-docs-by docs []))))

(testing "Sort by ascending age"
(let [sorted (operators/sort-docs-by docs [{:column "age" :direction :asc}])]
(is (= ["user:2" "user:4" "user:1" "user:3"] (map :id sorted)))))

(testing "Sort by descending age"
(let [sorted (operators/sort-docs-by docs [{:column "age" :direction :desc}])]
(is (= ["user:3" "user:1" "user:4" "user:2"] (map :id sorted)))))

(testing "Sort by multiple fields (dept asc, age desc)"
(let [sorted (operators/sort-docs-by docs [{:column "dept" :direction :asc}
{:column "age" :direction :desc}])]
(is (= ["user:4" "user:2" "user:3" "user:1"] (map :id sorted)))))))

(deftest test-apply-limit
(let [docs [{:id "user:1" :name "Alice"}
{:id "user:2" :name "Bob"}
{:id "user:3" :name "Charlie"}
{:id "user:4" :name "Diana"}
{:id "user:5" :name "Eve"}]]

(testing "Nil limit returns all docs"
(is (= 5 (count (operators/apply-limit docs nil)))))

(testing "Zero limit returns empty collection"
(is (empty? (operators/apply-limit docs 0))))

(testing "Limit less than total count"
(is (= 3 (count (operators/apply-limit docs 3))))
(is (= ["user:1" "user:2" "user:3"] (map :id (operators/apply-limit docs 3)))))

(testing "Limit equal to total count"
(is (= 5 (count (operators/apply-limit docs 5)))))

(testing "Limit greater than total count"
(is (= 5 (count (operators/apply-limit docs 10)))))))
Loading
Loading