diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a6ffaf6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +pathwise.clj diff \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c8cf06b..55ef874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## 1.5.0 (2025-06-20) + +* Allow unsorted maps and sets to be used as keys in paths using codes `0xa0` `0xa1`. +* Reserve all codes `0xa2` - `0xaf` for future use. + +Sorted maps and sets remain unsupported in paths. This is for several reasons including that the sorting criteria cannot be reliably encoded. + +*Note: if you have user-defined encoding types using codes between `0xa0` and `0xaf` a warning will be printed but as long as you do not use the new supported types (e.g. maps & sets) in paths your code should continue to work.* + ## 1.4.2 & 1.4.3 (2025-06-08) * bugfix: remove clj-time from project.clj diff --git a/README.md b/README.md index 46ecf17..ffc9727 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Codax is an idiomatic transactional embedded database for clojure. A codax datab [![Clojars Project](http://clojars.org/codax/latest-version.svg)](http://clojars.org/codax) +Version 1.5.0 allows unsorted sets and maps to be used as [path](#paths) keys. It also reserves pathwise encoding codes `0xa0` - `0xaf`. + Version 1.4.1 removes built-in support for clj-time. If you have an older database using clj-time see the [changelog](CHANGELOG.md) Version 1.4.0 implements [upgradable transactions](doc/upgradable-transactions.md). (using `with-upgradable-transaction` macro) and **fixes an RCE vulnerability**. @@ -102,6 +104,9 @@ A `path` is a vector of keys similar to the `[k & ks]` used in function like `as - false - nil - java.time.Instant + - unsorted maps + - unsorted sets + - the path can only target nested maps, and **cannot be used to descend into other data structures (e.g. vectors)**. - you can get the empty path (e.g. `(get-at db [])` returns the full database) but you cannot modify it (e.g. `(assoc-at [] :foo)` throws an error) diff --git a/doc/types.md b/doc/types.md index f08bd20..b53b458 100644 --- a/doc/types.md +++ b/doc/types.md @@ -7,7 +7,8 @@ It takes the following parameters: - `hex-code` - a number used for type identification in the database - `0x0` is used internally and off limits - - built-in codes which should **not be overwritten** are: `0x10` , `0x20`, `0x21`, `0x24`, `0x25`, `0x30`, `0x31`, `0x32`, `0x68`, `0x69`, `0x70`, `0xa0`. Attempts to overwrite them (or generally change encodings) should print a warning. + - built-in codes which should **not be overwritten** are: `0x10` , `0x20`, `0x21`, `0x24`, `0x25`, `0x30`, `0x31`, `0x32`, `0x68`, `0x69`, `0x70`, `0xa0`. Attempts to overwrite them (or generally change encodings) prints a warning. + - `0xa0` - `0xaf` are reserved for future use as of v1.5.0 - `types` - the types to be encoded (generally there should only be one) - `encoder` - a function which takes elements of the types specified in `types` and **returns a string representation** - `decoder` - a function which takes the string reprensentations generated by the encoder and reconstructs a value of the appropriate type diff --git a/project.clj b/project.clj index 2f6835a..8ff9f42 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject codax "1.4.3" +(defproject codax "1.5.0" :description "Codax is an idiomatic transactional embedded database for clojure" :url "https://github.com/dscarpetti/codax" :license {:name "Eclipse Public License" diff --git a/src/codax/pathwise.clj b/src/codax/pathwise.clj index 7873039..33619ce 100644 Binary files a/src/codax/pathwise.clj and b/src/codax/pathwise.clj differ diff --git a/test/codax/pathwise_test.clj b/test/codax/pathwise_test.clj index 0b7954e..87af715 100644 --- a/test/codax/pathwise_test.clj +++ b/test/codax/pathwise_test.clj @@ -145,7 +145,7 @@ ;;;; encoding decoding errors -(deftest no-encoder-for-element +#_(deftest no-encoder-for-element (try (do (encode #{}) @@ -156,7 +156,7 @@ (is (= type clojure.lang.PersistentHashSet)) (is (= element #{})))))) -(deftest no-decoder-for-element +#_(deftest no-decoder-for-element (try (do (decode (str (char 0x2) #{:unrecognized} (char 0x00))) @@ -213,12 +213,14 @@ (reduce str "" (take length (iterate (fn [_] (char (+ min-char (int (rand max-char))))) nil))))) -(declare gen-random-vector gen-random-element) +(declare gen-random-vector gen-random-element gen-random-set gen-random-map) -(defn gen-random-element [& {:keys [max-vector-length]}] - (let [num (rand 12)] +(defn gen-random-element [& {:keys [max-length]}] + (let [num (rand 16)] (cond - (> num 10) (gen-random-vector :max-length max-vector-length) + (> num 14) (gen-random-map :max-length max-length) + (> num 12) (gen-random-set :max-length max-length) + (> num 10) (gen-random-vector :max-length max-length) (> num 9) nil (> num 8) true (> num 7) false @@ -235,9 +237,27 @@ (let [max-length (if max-length (dec max-length) 15) length (int (Math/floor (rand max-length)))] (if (> length 0) - (vec (take (inc length) (rest (iterate (fn [_] (gen-random-element :max-vector-length max-length)) nil)))) + (vec (take (inc length) (rest (iterate (fn [_] (gen-random-element :max-length max-length)) nil)))) []))) +(defn gen-random-set [& {:keys [max-length]}] + (let [max-length (if max-length (dec max-length) 15) + length (int (Math/floor (rand max-length)))] + (if (> length 0) + (set (take (inc length) (rest (iterate (fn [_] (gen-random-element :max-length max-length)) nil)))) + #{}))) + +(defn gen-random-map [& {:keys [max-length]}] + (let [max-length (if max-length (dec max-length) 15) + length (int (Math/floor (rand max-length)))] + (if (> length 0) + (->> (iterate (fn [_] (gen-random-element :max-length max-length)) nil) + rest + (take (inc length)) + (partition 2) + (map vec) + (into {})) + {}))) (defn test-random [& {:keys [hide-success]}] (test-encoding (gen-random-vector) :hide-success hide-success)) @@ -260,3 +280,42 @@ (deftest random-path-test (is (test-many-random 100 :hide-success true))) + +(deftest map-ordering-1 + (is (= (encode {:a 1, {:k :b} 2}) + (encode {{:k :b} 2 :a 1})))) + +(deftest map-ordering-2 + (let [f1 {:a 1, {:k :b} 2} + e1 (encode f1) + d1 (decode e1) + f2 {{:k :b} 2 :a 1} + e2 (encode f2) + d2 (decode e2)] + (is (= e1 e2)) + (is (= f1 d1 f2 d2)))) + +(defn randomize-map-ordering [x] + (cond + (map? x) (into {} (shuffle (map (fn [[k v]] [k (randomize-map-ordering v)]) x))) + (set? x) (into #{} (map randomize-map-ordering x)) + (vector? x) (mapv randomize-map-ordering x) + :else x)) + +(deftest random-map-ordering + (loop [n 1000] + (when (pos? n) + (let [m1 (gen-random-map :max-length 12) + e1 (encode m1) + d1 (decode e1) + + m2 (randomize-map-ordering m1) + e2 (encode m2) + d2 (decode e2)] + (is (= e1 e2)) + (is (= m1 m2 d1 d2)) + (recur (dec n)))))) + +(deftest set-ordering-1 + (is (= (encode #{#{:a :b} :c {:a 1}}) + (encode #{{:a 1} :c #{:b :a}}))))