├── .gitignore ├── README.md ├── changelog.md ├── connections.md ├── doc └── intro.md ├── project.clj ├── src └── bitemyapp │ └── revise │ ├── connection.clj │ ├── core.clj │ ├── lambda.clj │ ├── protodefs.clj │ ├── protoengine.clj │ ├── query.clj │ ├── response.clj │ └── utils │ ├── bytes.clj │ ├── case.clj │ └── seq.clj └── test └── bitemyapp └── revise └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | .nrepl-port 14 | rethinkdb.proto 15 | src/bitemyapp/revise/testing.clj 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Revise 2 | 3 | Clojure RethinkDB client. Asynchronous, lock-free, efficient, and easy to use! 4 | 5 | Query RethinkDB using semantics familiar to any Clojure programmer. 6 | 7 | ## Stability 8 | 9 | Alpha-grade at this point, we're seeking people who want to use Clojure and RethinkDB to help us harden it up. 10 | 11 | We're confident this is already one of the more feature-complete community-maintained libraries. 12 | 13 | ## Leiningen 14 | 15 | !["Leiningen version"](https://clojars.org/revise/latest-version.svg) 16 | 17 | ## Connection Management 18 | 19 | [A brief explanation is here](connections.md) 20 | 21 | ## Introduction 22 | 23 | These docs are - for now - loosely based on the python api docs. The driver 24 | works on version `1.9` and `1.10` (in our testing so far) of RethinkDB. 25 | 26 | ## Usage 27 | 28 | ```clojure 29 | (require '[bitemyapp.revise.connection :refer [connect close]]) 30 | (require '[bitemyapp.revise.query :as r]) 31 | (require '[bitemyapp.revise.core :refer [run run-async]]) 32 | 33 | ;; connect returns the connection agent 34 | (let [local-conn (connect) ;; defaults to localhost 35 | ;; pass in connection options map to specify beyond the defaults 36 | remote-conn (connect {:host "99.99.99.1" 37 | :port 28015 38 | :auth-key ""}) 39 | ;; Run a query and return the result. Blocks as long as it needs to 40 | ;; get a result (or an error) 41 | response1 (-> (r/db "test") (r/table-create-db "authors") (run local-conn)) 42 | ;; We may be having issues so we specify a timeout to run 43 | response2 (-> (r/db "test") (r/table-list-db) (run remote-conn 15000)) 44 | ;; We want to run a query asynchronously - giving up the error handling 45 | response3 (-> (r/db-list) (run-async local-conn))] 46 | ;; dereference the promise to block on it. 47 | (println @response3) 48 | ;; We are done using the local connection 49 | (close local-conn)) 50 | ``` 51 | 52 | ## Connecting to RethinkDB 53 | 54 | Inside the namespace `bitemyapp.revise.connection` there are 2 functions we need: 55 | 56 | * `connect` `([& [conn-map]])` 57 | * `close` `([conn])` 58 | 59 | `connect` takes an optional connection map to override any or all of the default 60 | values: 61 | 62 | * `:host` `"127.0.0.1"` 63 | * `:port` `28015` 64 | * `:token` `0` The token of the first query to the connection. Autoincrements. 65 | * `:auth-key` `""` The authentication key. 66 | 67 | Connect will return an agent to which you can send queries. 68 | 69 | To close the connection use the function `close` with the agent as argument. 70 | 71 | ## Sending queries 72 | 73 | Inside the namespace `bitemyapp.revise.core` there are again 2 functions we need: 74 | 75 | * `run` `([query connection & [timeout]])` 76 | * `run-async` `([query connection])` 77 | 78 | Our queries are compiled and sent to the connection using those two functions. 79 | 80 | `run` takes an optional timeout in milliseconds (default `10000`) and will block 81 | until it has a response or it times out. It will throw when it times out or the 82 | agent dies due to an exception when sending a query. 83 | 84 | `run` will return a map which includes the autoincrementing `token` that was 85 | implicitly sent to the agent and either a `:response` in case the query was 86 | successful or an `:error`, `:response` and `:backtrace` in case there was 87 | an error with our request (in this case the driver doesn't throw an exception). 88 | 89 | Alternatively we might decide to use `run-async` to send and run queries 90 | asynchronously. This will return us a promise which we can dereference. 91 | 92 | Note that `run-async` gives up the error handling of `run`. The agent _might_ 93 | die and you will have to check for it manually. 94 | 95 | After dereferencing the promise the return value will be the same as `run`. 96 | 97 | ## Compiling a query manually 98 | 99 | `run` and `run-async` have an implicit call to `bitemyapp.revise.protoengine/compile-term`. 100 | This compiles the query into protocol buffers. If you know about the official 101 | RethinkDB API and you want to inspect the protocol buffers Revise gives you, you 102 | can compile a query using that function. To send manually compiled queries to the 103 | database, use `send-term` in the `bitemyapp.revise.connection` namespace. 104 | That will be the equivalent of using `run-async`. 105 | 106 | ## API 107 | 108 | The api is under the namespace bitemyapp.revise.query. 109 | 110 | ```clojure 111 | (require '[bitemyapp.revise.query :as r]) 112 | ``` 113 | 114 | Note: rethinkdb doesn't let you use hyphens (`-`) as part of database or table 115 | names. Revise won't 'fix' those names for you. 116 | 117 | Also note that keywords and strings are interchangeable. 118 | 119 | ### Lambdas 120 | 121 | Many queries such as `map`, `filter`, etc. support lambdas. Lambdas are anonymous 122 | functions with syntax like clojure's `fn`. 123 | 124 | Example: 125 | 126 | ```clojure 127 | (-> [1 2 3 4 5 6 7] 128 | (r/map (r/lambda [n] 129 | (r/* n 2))) 130 | (run conn)) 131 | ``` 132 | 133 | This will give you the response `([2 4 6 8 10 12 14])` 134 | 135 | ### Manipulating databases 136 | 137 | #### db-create 138 | `([db-name])` 139 | 140 | Create a database. 141 | 142 | ```clojure 143 | (-> (r/db-create "my_db") (run conn)) 144 | ``` 145 | 146 | #### db-drop 147 | `([db-name])` 148 | 149 | Drop a database. 150 | 151 | ```clojure 152 | (-> (r/db-drop "my_db") (run conn)) 153 | ``` 154 | 155 | #### db-list 156 | `([])` 157 | 158 | List the database names in the system. 159 | 160 | ```clojure 161 | (-> (r/db-list) (run conn)) 162 | ``` 163 | 164 | ### Manipulating tables 165 | 166 | #### table-create-db 167 | 168 | `([db table-name & {:as optargs}])` 169 | 170 | Create a table on the specified database. The following options are available: 171 | 172 | * `:primary-key` The name of the primary key. Default: `:id`. 173 | * `:durability` If set to `:soft`, this enables soft durability on this table: 174 | writes will be acknowledged by the server immediately and flushed to disk in the 175 | background. Default is `:hard` (acknowledgement of writes happens after data has been 176 | written to disk). 177 | * `:cache-size` Set the cache size (in bytes) to be used by the table. 178 | The default is 1073741824 (1024MB). 179 | * `:datacenter` The name of the datacenter this table should be assigned to. 180 | 181 | ```clojure 182 | (-> (r/db "test") (r/table-create-db "authors") (run conn)) 183 | (-> (r/db "test") (r/table-create-db "users" :primary-key :email) (run conn)) 184 | ``` 185 | 186 | #### table-create 187 | 188 | `([table-name & {:as optargs}])` 189 | 190 | Like `table-create-db` except that the db is the default db. 191 | 192 | ```clojure 193 | (-> (r/table-create "authors") (run conn)) 194 | ``` 195 | 196 | #### table-drop-db 197 | 198 | `([db table-name])` 199 | 200 | Drop a table from a specific db. The table and all its data will be deleted. 201 | 202 | ```clojure 203 | (-> (r/db "test") (r/table-drop-db "authors") (run conn)) 204 | ``` 205 | 206 | #### table-drop 207 | 208 | `([table-name])` 209 | 210 | Like `table-drop-db` except the default db is used. 211 | 212 | ```clojure 213 | (-> (r/table-drop "authors") (run conn)) 214 | ``` 215 | 216 | #### index-create 217 | 218 | `([table index-name lambda1 & [multi?]])` 219 | 220 | Create a new secondary index with a given name on the specified table. 221 | 222 | ```clojure 223 | (-> (r/table "authors") 224 | (r/index-create :author 225 | (r/lambda [author] 226 | (r/get-field author :name))) 227 | (run conn)) 228 | ;; Compound index 229 | (-> (r/table "authors") 230 | (r/index-create :name-tv-show 231 | (r/lambda [author] 232 | [(r/get-field author :name) 233 | (r/get-field author :tv-show)])) 234 | (run conn)) 235 | ;; A multi index. The r/lambda of a multi index should return an array. It will allow 236 | ;; you to query based on whether a value is present in the returned array 237 | (-> (r/table "authors") 238 | (r/index-create :posts 239 | (r/lambda [author] 240 | (r/get-field author :posts)) ; returns an array 241 | true) ; :multi -> true 242 | (run conn)) 243 | ``` 244 | 245 | #### index-drop 246 | 247 | `([table index-name])` 248 | 249 | Delete a previously created secondary index of this table. 250 | 251 | ```clojure 252 | (-> (r/table "authors") (r/index-drop :posts) (run conn)) 253 | ``` 254 | 255 | #### index-list 256 | 257 | `([table])` 258 | 259 | List all the secondary indexes of this table. 260 | 261 | ```clojure 262 | (-> (r/table "authors") (r/index-list) (run conn)) 263 | ``` 264 | 265 | ### Writing data 266 | #### insert 267 | 268 | `([table data & {:as optargs}])` 269 | 270 | Insert json documents into a table. Accepts a single json document (a clojure map) 271 | or an array of documents (a clojure vector of clojure maps). 272 | 273 | Accepts the following options: 274 | 275 | * `:upsert` A `bool`. Default is true. If true it will overwrite documents that 276 | already exist. 277 | * `:durability` `:soft` or `:hard`. Override the durability of the table for this 278 | operation. 279 | * `:return-vals` A `bool`. Only valid for single object inserts. If `true` you 280 | get back the row you inserted on the key `:nev_val`. And if you overwrote a row 281 | it will be in `:old_val` 282 | 283 | 284 | ```clojure 285 | (def authors [{:name "William Adama" :tv-show "Battlestar Galactica" 286 | :posts [{:title "Decommissioning speech", 287 | :rating 3.5 288 | :content "The Cylon War is long over..."}, 289 | {:title "We are at war", 290 | :content "Moments ago, this ship received word..."}, 291 | {:title "The new Earth", 292 | :content "The discoveries of the past few days..."}]} 293 | 294 | {:name "Laura Roslin", :tv-show "Battlestar Galactica", 295 | :posts [{:title "The oath of office", 296 | :rating 4 297 | :content "I, Laura Roslin, ..."}, 298 | {:title "They look like us", 299 | :content "The Cylons have the ability..."}]}]) 300 | 301 | (def jean-luc {:name "Jean-Luc Picard", :tv-show "Star Trek TNG", 302 | :posts [{:title "Civil rights", 303 | :content "There are some words I've known since..."}]}) 304 | 305 | (-> (r/table "authors") 306 | (r/insert authors) 307 | (run conn)) 308 | 309 | (-> (r/table "authors") 310 | (r/insert jean-luc :return-vals true) 311 | (run conn)) 312 | ``` 313 | 314 | Insert returns a map with the following attributes: 315 | 316 | * `:inserted` The number of documents that were succesfully inserted. 317 | * `:replaced` The number of documents that were updated when upsert is used. 318 | * `:unchanged` The number of documents that would have been modified, except that 319 | the new value was the same as the old value when doing an upsert. 320 | * `:errors` The number of errors encountered while inserting; if errors were 321 | encountered while inserting, first_error contains the text of the first error. 322 | * `:generated_keys` A list of generated primary key values deleted and skipped: 323 | 0 for an insert operation. 324 | 325 | If you specified :return-vals true you will also get the following keys: 326 | * `:nev_val` The value of the object you inserted 327 | * `:old_val` The value of the object you overwrote (`nil` if you didn't) 328 | 329 | #### update 330 | 331 | `([stream-or-single-selection lambda1-or-obj])` 332 | 333 | Update JSON documents in a table. Accepts a JSON document (clojure map), a RQL 334 | expression or a combination of the two. Accepts the following optional keys: 335 | 336 | * `:durability` `:soft` or `:hard` - Override the table's durability for this 337 | operation. 338 | * `:return-vals` A `bool`. Only valid for single-row modifications. If `true` 339 | return the new value in `:new_val` and the old value in `:old_val`. 340 | * `non-atomic` A `bool`. Allow the server to run non-atomic operations. 341 | 342 | ```clojure 343 | ;; Make all authors be fictional 344 | (-> (r/table "authors") (r/update {:type "fictional"})) 345 | ;; Add the rank of admiral to William Adama 346 | (-> (r/table "authors") 347 | (r/filter (r/lambda [row] 348 | (r/= "William Adama" 349 | (r/get-field row :name)))) 350 | (r/update {:rank "Admiral"}) 351 | (run conn)) 352 | ;; Add a post to Jean-Luc 353 | (-> (r/table "authors") 354 | (r/filter (r/lambda [row] 355 | (r/= "Jean-Luc Picard" 356 | (r/get-field row :name)))) 357 | (r/update 358 | (r/lambda [row] 359 | {:posts 360 | (r/append (r/get-field row :posts) 361 | {:title "Shakespeare" 362 | :content "What a piece of work is man.."})})) 363 | (run conn)) 364 | ``` 365 | 366 | Update returns a map that contains the following attributes: 367 | 368 | * `:replaced` The number of documents that were updated. 369 | * `:unchanged` The number of documents that would have been modified except the new 370 | value was the same as the old value. 371 | * `:skipped` The number of documents that were left unmodified because there was 372 | nothing 373 | to do: either the row didn't exist or the new value is null. 374 | * `:errors` The number of errors encountered while performing the update; if errors 375 | occured, first_error contains the text of the first error. 376 | * `:deleted` and `:inserted` Are 0 for an update operation. 377 | 378 | #### replace 379 | 380 | `([stream-or-single-selection lambda1-or-obj & {:as optargs}])` 381 | 382 | Replace documents in a table. The new document must have the same primary key as the 383 | original document. Accepts the following optional arguments: 384 | 385 | * `:non-atomic` Allow non-atomic updates. 386 | * `:durability` `:soft` or `:hard`. Override the table or query's default 387 | durability setting. 388 | * `:return-vals` A `bool` Return the old and new values of the row you're 389 | modifying when set to true (only valid for single row replacements). 390 | 391 | ```clojure 392 | ;; Assuming :name is the primary key on the table 393 | (-> (r/table "authors") (r/get "Wooster") 394 | (r/replace {:tv-show "Jeeves"} :return-vals true) 395 | (run conn)) 396 | ``` 397 | 398 | #### delete 399 | 400 | `([stream-or-single-selection & {:as optargs}])` 401 | 402 | Delete the rows in a selection. Accepts the following optional arguments: 403 | 404 | * `:durability` Default: `:soft`; Override the table or query's default durability 405 | setting. Other possible values: `:hard` 406 | * `:return-vals` Default: `true`; Return the old value of the row you're deleting 407 | when set to true (only valid for single row deletes) on the key `:old_val` 408 | 409 | ```clojure 410 | (-> (r/table "authors") 411 | (r/filter (r/lambda [row] 412 | (r/< (r/count (r/get-field row :posts)) 413 | 3))) 414 | (r/delete) 415 | (run conn)) 416 | ``` 417 | 418 | `delete` returns a map with the following attributes: 419 | 420 | * `:deleted` The number of documents that were deleted. 421 | * `:skipped` The number of documents from the selection that were left unmodified 422 | because there was nothing to do. For example, if you delete a row that has already 423 | been deleted, that row will be skipped. 424 | * `:errors` The number of errors encountered while deleting if errors occured, 425 | first_error contains the text of the first error. 426 | * `:inserted` Replaced, and unchanged: all 0 for a delete operation. 427 | 428 | If you deleted only one row and `return-vals` is `true` then you also get the 429 | following keys: 430 | 431 | * `:new_val` Is nil. 432 | * `:old_val` Contains the value of the document you deleted 433 | 434 | ### Selecting data 435 | 436 | #### db 437 | 438 | `([db-name])` 439 | 440 | Reference a database. This will give you an error if you try to run it. If you want 441 | a list of tables use `r/table-list-db` 442 | 443 | ```clojure 444 | (r/db "test") 445 | (r/db :test) 446 | ``` 447 | 448 | #### table-db 449 | 450 | `([db table-name])` 451 | 452 | Select all documents on a table. This command can be chained with other commands to 453 | do further processing on the data 454 | 455 | ```clojure 456 | (-> (r/db "test") (r/table-db "authors") (run conn)) 457 | ``` 458 | 459 | #### table 460 | 461 | `([table-name])` 462 | 463 | Like table-db except that it uses the default database. 464 | 465 | ```clojure 466 | (-> (r/table "authors") (run conn)) 467 | ``` 468 | 469 | #### get 470 | 471 | `([table key])` 472 | 473 | Get a document by its primary key. 474 | 475 | ```clojure 476 | ;; After setting the secondary index :name on the table "authors" 477 | (-> (r/table "authors") (r/get "7644aaf2-9928-4231-aa68-4e65e31bf219") 478 | (run conn)) 479 | ``` 480 | 481 | #### get-all 482 | 483 | `([table keys-vec & [index]])` 484 | 485 | Get all documents where the given value matches the value of the requested index 486 | 487 | ```clojure 488 | ;; After setting the secondary key :name on the table :authors 489 | (-> (r/table "authors") 490 | (r/get-all ["William Adama"] :name) 491 | (run conn)) 492 | ``` 493 | 494 | #### between 495 | 496 | `([stream-selection lower-key upper-key & [index]])` 497 | 498 | Get all documents between two keys. index can be the name of a secondary index. 499 | `[lower-key upper-key)` 500 | 501 | ```clojure 502 | ;; Assuming the primary key on our table is a number. 503 | (-> (r/table "authors") (r/between 10 20) (run conn)) 504 | ``` 505 | 506 | #### filter 507 | 508 | `([sequence lambda1-or-obj & [default-val]])` 509 | 510 | Filter a sequence with either a function or a shortcut object. 511 | The body of `filter` is wrapped in an implicit `(default .. false)` and you 512 | can change the default value by specifying the `default-val` optarg. If you 513 | make the default `(error)`, all errors caught by default will be rethrown 514 | as if the default did not exist 515 | 516 | ```clojure 517 | (-> (r/table "authors") 518 | (r/filter (r/lambda [row] 519 | (r/= (r/get-field row :name) "William Adama"))) 520 | (run conn)) 521 | ``` 522 | 523 | ### Joins 524 | 525 | #### inner-join 526 | 527 | `([sequence1 sequence2 predicate])` 528 | 529 | Returns the inner product of two sequences (e.g. a table and a filter result) filtered 530 | by the predicate. The query compares each row of the left sequence with each row of 531 | the right sequence to find all pairs of rows which satisfy the predicate (a `lambda` 532 | of two arguments). When the predicate is satisfied, each matched pair of rows of both 533 | sequences are combined into a result row. 534 | 535 | ```clojure 536 | (-> (r/table "marvel") 537 | (r/inner-join (r/table "dc") 538 | (lambda [marvel-row dc-row] 539 | (r/< (get-field marvel-row :strength) 540 | (get-field dc-row :strength)))) 541 | (run conn)) 542 | ``` 543 | 544 | #### outer-join 545 | 546 | `([sequence1 sequence2 predicate])` 547 | 548 | Computes a left outer join by retaining each row in the left table even if no match 549 | was found in the right table. 550 | 551 | ```clojure 552 | (-> (r/table "marvel") 553 | (r/outer-join (r/table "dc") 554 | (r/lambda [marvel-row dc-row] 555 | (r/< (get-field marvel-row :strength) 556 | (get-field dc-row :strength)))) 557 | (run conn)) 558 | ``` 559 | 560 | #### eq-join 561 | 562 | `([sequence1 left-attr sequence2 & [index]])` 563 | 564 | An efficient join that looks up elements in the right table by primary key. 565 | `index` defaults to `:id` 566 | 567 | ```clojure 568 | (-> (r/table "marvel") (r/eq-join "main_dc_collaborator" (r/table "dc")) 569 | (run conn)) 570 | ``` 571 | 572 | #### zip 573 | 574 | `([sequence])` 575 | 576 | Used to 'zip' up the result of a join by merging the 'right' fields into 'left' fields 577 | of each member of the sequence. 578 | 579 | ```clojure 580 | (-> (r/table "marvel") (r/eq-join "main_dc_collaborator" (r/table "dc")) 581 | (r/zip) 582 | (run conn)) 583 | ``` 584 | 585 | ### Transformations 586 | 587 | #### map 588 | 589 | `([sequence lambda1])` 590 | 591 | Transform each element of the sequence by applying the given mapping function. 592 | 593 | ```clojure 594 | (-> (r/table "authors") 595 | (r/map (r/lambda [author] 596 | (r/count (r/get-field author :posts)))) 597 | (run conn)) 598 | ``` 599 | 600 | #### with-fields 601 | 602 | `([sequence & pathspecs])` 603 | 604 | Takes a sequence of objects and a variable number of fields. If any objects in the 605 | sequence don't have all of the specified fields, they're dropped from the sequence. 606 | The remaining objects have the specified fields plucked out. Identical to has-fields 607 | followed by pluck. 608 | 609 | ```clojure 610 | ;; Get a list of authors and their posts, excluding any authors that lack one. 611 | (-> (r/table "authors") (r/with-fields :name :posts) 612 | (run conn)) 613 | ``` 614 | 615 | #### mapcat 616 | 617 | `([sequence lambda1])` 618 | 619 | Map a function over a sequence and then concatenate the results together 620 | 621 | ```clojure 622 | ;; Get all of the posts of all authors 623 | (-> (r/table "authors") 624 | (r/mapcat (r/lambda [author] 625 | (r/get-field author :posts))) 626 | (run conn)) 627 | ``` 628 | 629 | #### order-by 630 | 631 | `([sequence & keys-or-orderings])` 632 | 633 | Sort the sequence by document values of the given key(s). Defaults to ascending 634 | ordering. To specify order, wrap the key with `(r/asc ..)` or `(r/desc ..)` 635 | 636 | ```clojure 637 | (-> (r/table "marvel") 638 | (r/order-by :enemies_vanquished :damsels_saved) 639 | (run conn)) 640 | ``` 641 | 642 | #### skip 643 | 644 | `([sequence n])` 645 | 646 | Skip a number of elements from the head of the sequence 647 | 648 | ```clojure 649 | ;; Ignore the first authors sorted alphabetically 650 | (-> (r/table "authors") 651 | (r/order-by :name) 652 | (r/skip 2) 653 | (run conn)) 654 | ``` 655 | 656 | #### limit 657 | 658 | `([sequence n])` 659 | 660 | End the sequence after the given number of elements 661 | 662 | ```clojure 663 | ;; Get 10 posts from all of our authors 664 | (-> (r/table "authors") 665 | (r/mapcat (r/lambda [author] 666 | (r/get-field author :posts))) 667 | (r/limit 10) 668 | (run conn)) 669 | ``` 670 | 671 | #### slice 672 | 673 | `([sequence start-index end-index])` 674 | 675 | Trim the sequence to within the bounds provided. 676 | 677 | ```clojure 678 | (-> (r/table "marvel") 679 | (r/order-by :strength) 680 | (r/slice 5 10) 681 | (run conn)) 682 | ``` 683 | 684 | #### nth 685 | 686 | `([sequence idx])` 687 | 688 | Get the nth element of a sequence. Zero indexed. 689 | 690 | ```clojure 691 | (-> (r/table "authors") 692 | (r/nth 1) 693 | (run conn)) 694 | ``` 695 | 696 | #### indexes-of 697 | 698 | `([sequence item-or-predicate])` 699 | 700 | Get the indexes of an element in a sequence. If the argument is a predicate, get the 701 | indexes of all elements matching it. 702 | 703 | ```clojure 704 | (-> (r/indexes-of ["a" "b" "c"] "c") (run conn)) 705 | ``` 706 | 707 | #### empty? 708 | 709 | `([sequence])` 710 | 711 | Test if a sequence is empty. 712 | 713 | ```clojure 714 | (-> (r/table "authors") 715 | (r/empty?) 716 | (run conn)) 717 | ``` 718 | 719 | #### union 720 | 721 | `([sequence1 sequence2])` 722 | 723 | Concatenate 2 sequences 724 | 725 | ```clojure 726 | (-> (r/table "marvel") 727 | (r/union 728 | (r/table "dc")) 729 | (run conn)) 730 | ``` 731 | 732 | #### sample 733 | 734 | `([sequence n])` 735 | 736 | Select a number of elements from the sequence with uniform random distribution. 737 | 738 | ```clojure 739 | (-> (r/table "authors") 740 | (r/sample 2) 741 | (run conn)) 742 | ``` 743 | 744 | ### Aggregation 745 | 746 | Compute smaller values from large sequences. 747 | 748 | #### reduce 749 | 750 | `([sequence lambda2 & [init-val]])` 751 | 752 | Produce a single value from a sequence through repeated application of a reduction 753 | function. 754 | 755 | ```clojure 756 | ;; How many posts are there? 757 | (-> (r/table "authors") 758 | (r/map (r/lambda [author] (r/count (r/get-field :posts)))) 759 | (r/reduce (r/lambda [acc next] (r/+ acc next)) 0) 760 | (run conn)) 761 | ``` 762 | 763 | #### count 764 | 765 | `([sequence & [filter]])` 766 | 767 | Count the number of elements in the sequence. With a single argument, count the number 768 | of elements equal to it. If the argument is a function, it is equivalent to calling 769 | filter before count. 770 | 771 | ```clojure 772 | (-> (r/table "authors") 773 | (r/count) 774 | (run conn)) 775 | ``` 776 | 777 | #### distinct 778 | 779 | `([sequence])` 780 | 781 | Remove duplicates from the sequence. 782 | 783 | ```clojure 784 | (-> (r/table "marvel") 785 | (r/mapcat (r/lambda [hero] 786 | (r/get-field hero :villain-list))) 787 | (r/distinct) 788 | (run conn)) 789 | ``` 790 | 791 | #### grouped-map-reduce 792 | 793 | `([sequence grouping mapping reduction & [base]])` 794 | 795 | Partition the sequence into groups based on the `grouping` function. The elements of 796 | each group are then mapped using the `mapping` function and reduced using the 797 | `reduction` function. Generalized form of group-by. 798 | 799 | ```clojure 800 | ;; Compare heroes against their weight class 801 | (-> (r/table "marvel") 802 | (r/grouped-map-reduce 803 | (r/lambda [hero] (r/get-field :weight-class)) ; grouping 804 | (r/lambda [hero] (r/pluck hero :name :strength)) :mapping 805 | (r/lambda [acc hero] 806 | (r/branch (r/< (r/get-field acc :strength) ; if 807 | (r/get-field hero :strength)) 808 | hero ; then 809 | acc ; else 810 | )) 811 | {:name "none" :strength 0}) ; base 812 | (run conn)) 813 | ``` 814 | 815 | #### group-by 816 | 817 | `([sequence keys-array operation-map])` 818 | 819 | Groups a sequence by one or more attributes and then applies a reduction. 820 | The third argument is a special literal giving the kind of operation 821 | to be performed and anay necessary arguments. 822 | 823 | At present group-by supports the following operations 824 | * :count - count the size of the group 825 | * {:sum attr} - sum the values of the given attribute accross the group 826 | * {:avg attr} - average the values of the given attribute accross the group" 827 | 828 | ```clojure 829 | (-> (r/table "marvel") 830 | (r/group-by [:weight-class] {:avg :strength}) 831 | (run conn)) 832 | 833 | (-> (r/table "marvel") 834 | (r/group-by [:age :weight-class] :count) 835 | (run conn)) 836 | 837 | (-> (r/table "marvel") 838 | (r/group-by [:weight-class] {:sum :foes-defeated}) 839 | (run conn)) 840 | ``` 841 | 842 | #### contains? 843 | 844 | `([sequence item-or-lambda1])` 845 | 846 | Returns whether or not a sequence contains the specified value, or if functions are 847 | provided instead, returns whether or not a sequence contains values matching all the 848 | specified functions. 849 | 850 | ```clojure 851 | (-> (r/table "marvel") 852 | (r/get "ironman") 853 | (r/get-field "opponents") 854 | (r/contains? "superman") 855 | (run conn)) 856 | ``` 857 | 858 | ### Document manipulation 859 | 860 | #### pluck 861 | 862 | `([object-or-sequence & selectors])` 863 | 864 | Get a subset of an object by selecting some attributes to preserve, 865 | or map that over a sequence 866 | 867 | ```clojure 868 | (-> (r/table "marvel") 869 | (r/get "IronMan") 870 | (r/pluck :reactor-state :reactor-power) 871 | (run conn)) 872 | ``` 873 | 874 | #### without 875 | 876 | `([object-or-sequence & pathspecs])` 877 | 878 | The opposite of pluck. Get a subset of an object by selecting some attributes to 879 | discard, or map that over a sequence. 880 | 881 | ```clojure 882 | (-> (r/table "marvel") (r/get "IronMan") (without :personal-victories-list) 883 | (run conn)) 884 | ``` 885 | 886 | #### merge 887 | 888 | `([& objects])` 889 | 890 | Merge objects. Right-preferential. 891 | 892 | ```clojure 893 | (-> (r/table "marvel") (r/get "IronMan") 894 | (r/merge (-> (r/table "loadouts") 895 | (r/get :alien-invasion-kit))) 896 | (run conn)) 897 | ``` 898 | 899 | The query `literal` takes a single argument and it can be used to indicate merge 900 | to replace the other object rather than merge it. 901 | 902 | #### append 903 | 904 | `([sequence item])` 905 | 906 | Append a value to an array 907 | 908 | ```clojure 909 | (-> (r/table "authors") 910 | (r/filter (r/lambda [author] 911 | (r/= "William Adama" 912 | (r/get-field author name)))) 913 | (r/update (r/lambda [author] 914 | {:posts 915 | (r/append (r/get-field row :posts) 916 | ;; Appending a new post 917 | {:title "Earth" 918 | :content "Earth is a dream.."})))) 919 | (run conn)) 920 | ``` 921 | 922 | #### prepend 923 | 924 | `([array item])` 925 | 926 | Prepend a value to an array 927 | 928 | ```clojure 929 | (-> (r/table "authors") 930 | (r/filter (r/lambda [author] 931 | (r/= "William Adama" 932 | (r/get-field author name)))) 933 | (r/update (r/lambda [author] 934 | {:posts 935 | (r/prepend (r/get-field row :posts) 936 | ;; Prepend a post 937 | {:title "Cylons" 938 | :content "The cylon war is long over"})))) 939 | (run conn)) 940 | ``` 941 | 942 | #### difference 943 | 944 | `([array1 array2])` 945 | 946 | Remove the elements of one array from another array. 947 | 948 | ```clojure 949 | (-> (r/table "marvel") 950 | (r/get "IronMan") 951 | (r/get-field :equipment) 952 | (r/difference "Boots") 953 | (run conn)) 954 | ``` 955 | 956 | #### set-insert 957 | 958 | `([array item])` 959 | 960 | Add a value to an array as if the array was a set. 961 | 962 | ```clojure 963 | (-> (r/table "marvel") 964 | (r/get "IronMan") 965 | (r/get-field "equipment") 966 | (r/set-insert "new-boots") 967 | (run conn)) 968 | ``` 969 | 970 | #### set-union 971 | 972 | `([array1 array2])` 973 | 974 | Add several values to an array as if it was a set 975 | 976 | ```clojure 977 | (-> (r/table "marvel") 978 | (r/get "IronMan") 979 | (r/get-field "equipment") 980 | (r/set-union ["new-boots" "arc-reactor"]) 981 | (run conn)) 982 | ``` 983 | 984 | #### set-intersection 985 | 986 | `([array1 array2])` 987 | 988 | Intersect 2 arrays returning values that occur in both of them as a set. 989 | 990 | ```clojure 991 | (-> (r/table "marvel") 992 | (r/get "IronMan") 993 | (r/get-field "equipment") 994 | (r/set-intersection ["new-boots" "arc-reactor"]) 995 | (run conn)) 996 | ``` 997 | 998 | #### get-field 999 | 1000 | `([sequence-or-object])` 1001 | 1002 | Get a single field from an object. If called on a sequence, gets that field from 1003 | every object in the sequence, skipping objects that lack it. 1004 | 1005 | ```clojure 1006 | (-> (r/table "marvel") 1007 | (r/get "IronMan") 1008 | (r/get-field "first-appearance") 1009 | (run conn)) 1010 | ``` 1011 | 1012 | #### has-fields? 1013 | 1014 | `([object & pathspecs])` 1015 | 1016 | Check whether an object contains all the specified fields or filters a 1017 | sequence so that al objects inside of it contain all the specified fields 1018 | 1019 | ```clojure 1020 | (-> (r/table "marvel") 1021 | (r/has-fields "spouse") 1022 | (run conn)) 1023 | ``` 1024 | 1025 | #### insert-at 1026 | 1027 | `([array idx item])` 1028 | 1029 | Insert a value in to an array at a given index. 1030 | 1031 | ```clojure 1032 | (-> ["IronMan" "SpiderMan"] 1033 | (r/insert-at 1 "Hulk") 1034 | (run conn)) 1035 | ``` 1036 | 1037 | #### splice-at 1038 | 1039 | `([array1 idx array2])` 1040 | 1041 | Insert several values into an array at a given index. 1042 | 1043 | ```clojure 1044 | (-> ["IronMan" "SpiderMan"] 1045 | (r/splice-at 1 ["Hulk" "Thor"]) 1046 | (run conn)) 1047 | ``` 1048 | 1049 | #### delete-at 1050 | 1051 | `([array idx & [end-idx]])` 1052 | 1053 | Remove an element from an array at a given index. 1054 | 1055 | ```clojure 1056 | (-> ["IronMan" "Hulk" "SpiderMan"] 1057 | (r/delete-at 1) 1058 | (run conn)) 1059 | ``` 1060 | 1061 | #### change-at 1062 | 1063 | `([array idx item])` 1064 | 1065 | Change a value in an array at a given index. 1066 | 1067 | ```clojure 1068 | (-> ["IronMan" "Bruce" "SpiderMan"] 1069 | (r/change-at 1 "Hulk") 1070 | (run conn)) 1071 | ``` 1072 | 1073 | #### keys 1074 | 1075 | `([object-or-single-selection])` 1076 | 1077 | Return an array containing all of the object's keys 1078 | 1079 | ```clojure 1080 | (-> (r/table "authors") 1081 | (r/get "7644aaf2-9928-4231-aa68-4e65e31bf219") 1082 | (r/keys) 1083 | (run conn)) 1084 | ``` 1085 | 1086 | ### String manipulation 1087 | 1088 | #### match 1089 | 1090 | `([str regexp])` 1091 | 1092 | Returns a match object if the string matches the regexp. Accepts RE2 syntax 1093 | https://code.google.com/p/re2/wiki/Syntax Accepts clojure regexp. 1094 | 1095 | ```clojure 1096 | (-> (r/table "users") 1097 | (r/filter (r/lambda [user] 1098 | (r/match (r/get-field user :name) 1099 | #"^A"))) 1100 | (run conn)) 1101 | ``` 1102 | 1103 | ### Math and logic 1104 | 1105 | The following symbols are also part of the api and they should be properly namespace 1106 | qualified: 1107 | 1108 | `r/+` Add numbers or concatenate strings or arrays. 1109 | 1110 | `r/-` Substract numbers. 1111 | 1112 | `r/*` Multiply numbers or make a periodic array. 1113 | 1114 | `r/div` Divide numbers. **Note that it's not r//** 1115 | 1116 | `r/mod` Find the remainder of two numbers. 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | `r/=` Test for equality. 1123 | 1124 | `r/not=` Test for inequality. 1125 | 1126 | `r/>` Greater than. 1127 | 1128 | `r/>=` Greater equal. 1129 | 1130 | `r/<` Lower than. 1131 | 1132 | `r/<=` Lower equal. 1133 | 1134 | `r/not` Logical inverse. 1135 | 1136 | ### Dates and times 1137 | 1138 | #### now 1139 | 1140 | `([])` 1141 | 1142 | Return a time object representing the time in UTC. The command now() is computed once 1143 | when the server receives the query, so multiple instances of r.now() will always 1144 | return the same time inside a query. 1145 | 1146 | ```clojure 1147 | (-> (r/table "users") 1148 | (r/insert {:name "John" 1149 | :subscription-date (r/now)}) 1150 | (run conn)) 1151 | ``` 1152 | 1153 | #### time 1154 | 1155 | `([year month day & [timezone]] [year month day hour minute second & [timezone])` 1156 | 1157 | Create a time object for a specific time. Timezone is a string like: `"-06:00"` 1158 | 1159 | ```clojure 1160 | ;; Update the birthdate of the user "John" to November 3rd, 1986 UTC 1161 | (-> (r/table "user") 1162 | (r/get "John") 1163 | (r/update {:birthdate (r/time 1986 11 3 "Z")]) 1164 | (run conn)) 1165 | ``` 1166 | 1167 | #### epoch-time 1168 | 1169 | `([epoch-time])` 1170 | 1171 | Create a time object based on seconds since epoch. 1172 | 1173 | ```clojure 1174 | ;; Update the birthdate of the user "John" to November 3rd, 1986 1175 | (-> (r/table "user") 1176 | (r/get "john") 1177 | (r/update {:birthdate (r/epoch-time 531360000)}) 1178 | (run conn)) 1179 | ``` 1180 | 1181 | #### iso8601 1182 | 1183 | `([iso8601-date])` 1184 | 1185 | Create a time object based on an iso8601 date-time string. 1186 | 1187 | ```clojure 1188 | (-> (r/table "user") 1189 | (r/get "John") 1190 | (r/update {:birth (r/iso8601 "1986-11-03T08:30:00-07:00")}) 1191 | (run conn)) 1192 | ``` 1193 | 1194 | #### in-timezone 1195 | 1196 | `([time timezone])` 1197 | 1198 | Return a new time object with a different timezone. Results returned by functions 1199 | that take the timezone into account will be different. 1200 | 1201 | ```clojure 1202 | (-> (r/now) 1203 | (r/in-timezone "-08:00) 1204 | (r/hours) 1205 | (run conn)) 1206 | ``` 1207 | 1208 | #### timezone 1209 | 1210 | `([time])` 1211 | 1212 | Return the timezone of the time object 1213 | 1214 | ```clojure 1215 | (-> (r/table "user") 1216 | (r/filter (r/lambda [user] 1217 | (r/= "-07:00" 1218 | (-> (r/get-field user :subscription-date) 1219 | (r/timezone))))) 1220 | (run conn)) 1221 | ``` 1222 | 1223 | #### during 1224 | 1225 | `([time start-time end-time])` 1226 | 1227 | Returns whether the time is in the range [start-time end-time) 1228 | 1229 | ```clojure 1230 | (-> (r/table "posts") 1231 | (r/filter (r/lambda [post] 1232 | (-> (r/get-field :date) 1233 | (r/during (r/time 2013 12 1) (r/time 2013 12 10))))) 1234 | (run conn)) 1235 | ``` 1236 | 1237 | #### date 1238 | 1239 | `([time])` 1240 | 1241 | Return a new time object only based on the day, month and year 1242 | 1243 | ```clojure 1244 | (-> (r/table "users") 1245 | (r/filter (r/lambda [user] 1246 | (r/= (-> (r/now) (r/date)) 1247 | (r/get-field user :birthday)))) 1248 | (run conn)) 1249 | ``` 1250 | 1251 | #### time-of-day 1252 | 1253 | `([time])` 1254 | 1255 | Return the number of seconds elapsed since the beginning of the day stored in the 1256 | time object. 1257 | 1258 | ```clojure 1259 | ;; Posts submitted before noon 1260 | (-> (r/table "posts") 1261 | (r/filter (r/lambda [post] 1262 | (r/> (* 12 60 60) ; Can be left as clojure.core/* 1263 | (-> (r/get-field post :date) 1264 | (r/time-of-day))))) 1265 | (run conn)) 1266 | ``` 1267 | 1268 | ### Access time fields 1269 | 1270 | All of these take a `time` as the only argument. 1271 | 1272 | `r/year` Return the year of a time object. 1273 | 1274 | `r/month` Return the month as a number between 1 and 12. 1275 | 1276 | `r/day` Return the day as a number between 1 and 31. 1277 | 1278 | `r/day-of-week` Return the day of week as a number between 1 and 7 (ISO 8601). 1279 | 1280 | `r/day-of-year` Return the day of the year as a number between 1 and 366 (ISO 8601). 1281 | 1282 | `r/hours` Return the hour as a number between 0 and 23. 1283 | 1284 | `r/minutes` Return the minute in a time object as a number between 0 and 59. 1285 | 1286 | `r/seconds` Return the seconds in a time object as a number between 0 and 59.999 (double precision). 1287 | 1288 | #### ->iso8601 1289 | 1290 | `([time])` 1291 | 1292 | Convert a time object to its ISO 8601 format. 1293 | 1294 | ```clojure 1295 | (-> (r/now) 1296 | (r/to-iso8601) 1297 | (run conn)) 1298 | ``` 1299 | 1300 | #### ->epoch-time 1301 | 1302 | `([time])` 1303 | 1304 | Convert a time to its epoch time. 1305 | 1306 | ```clojure 1307 | (-> (r/now) 1308 | (r/->epoch-time) 1309 | (run conn)) 1310 | ``` 1311 | 1312 | ### Control structures 1313 | 1314 | #### branch 1315 | 1316 | `([test then else])` 1317 | 1318 | Like an if. The test can be any value. Truthiness appears to be similar to 1319 | clojure's (`false` and `nil` are falsey, everything else is truthy). 1320 | 1321 | ```clojure 1322 | (-> (r/table "marvel") 1323 | (r/map (r/lambda [hero] 1324 | (r/branch (r/<= 100 1325 | (r/get-field hero :victories)) 1326 | (r/+ (r/get-field hero :name) " is a superhero") ; then 1327 | (r/+ (r/get-field hero :name) " is a hero")))) ; else 1328 | (run conn)) 1329 | ``` 1330 | 1331 | #### or 1332 | 1333 | `([& bools])` 1334 | 1335 | Like clojure's short-circuiting `or` except that: It short circuits _inside_ 1336 | RethinkDB and it is a little inneficient in that if your "booleans" are queries 1337 | it will probably run them twice. 1338 | 1339 | The same truthy/falsey rules apply as with `branch`. 1340 | 1341 | ```clojure 1342 | (-> (r/or false false false true) (run conn)) 1343 | (-> (r/or false nil false "hello!" nil) (run conn)) 1344 | ``` 1345 | 1346 | #### and 1347 | 1348 | `([& bools])` 1349 | 1350 | Like clojure's short-circuiting `and` except that: It short circuits _inside_ 1351 | RethinkDB and it is a little inneficient in that if your "booleans" are queries 1352 | it will probably run them twice. 1353 | 1354 | The same truthy/falsey rules apply as with `branch`. 1355 | 1356 | ```clojure 1357 | (-> (r/and true true true) (run conn)) 1358 | (-> (r/and 1 2 3 nil) (run conn)) 1359 | ``` 1360 | 1361 | 1362 | #### any 1363 | 1364 | `([& bools])` 1365 | 1366 | A short circuiting or that returns a boolean 1367 | 1368 | ```clojure 1369 | (-> (r/any false false true) (run conn)) 1370 | ``` 1371 | 1372 | #### all 1373 | 1374 | `([& bools])` 1375 | 1376 | Returns true if all of its arguments are true (short-circuiting). 1377 | 1378 | ```clojure 1379 | (-> (r/all true true true) (run conn)) 1380 | ``` 1381 | 1382 | #### foreach 1383 | 1384 | `([sequence lambda1])` 1385 | 1386 | Calls its function with each entry in the sequence and executes the array of 1387 | terms that function returns. 1388 | 1389 | ```clojure 1390 | (-> (r/table "marvel") 1391 | (r/foreach (r/lambda [hero] 1392 | (-> (r/table "villains") 1393 | (r/get (r/get-field hero :villain-defeated))))) 1394 | (r/delete) 1395 | (run conn)) 1396 | ``` 1397 | 1398 | #### error 1399 | 1400 | `([& [s]])` 1401 | 1402 | Throw a runtime error. If called with no arguments inside the second argument to 1403 | default, re-throw the current error. 1404 | 1405 | ```clojure 1406 | (-> (r/error "kaput") (run conn)) 1407 | ``` 1408 | 1409 | #### default 1410 | 1411 | `([item-to-check item-or-lambda1])` 1412 | 1413 | Evaluates its first argument. If that argument returns NULL or throws an error 1414 | related to the absence of an expected value, default will either return its 1415 | second argument or execute it if it's a function. If the second argument is a 1416 | function it will be passed either the text of the error or NULL as its argument. 1417 | 1418 | ```clojure 1419 | (-> (r/table "projects") 1420 | (r/map (r/lambda [p] 1421 | (r/+ (r/default (r/get-field p :staff) 0) 1422 | (r/default (r/get-field p :management) 0)))) 1423 | (run conn)) 1424 | ``` 1425 | 1426 | #### parse-val 1427 | 1428 | `([item])` 1429 | 1430 | Parse a clojure value to construct a json value. Strings, keywords, numbers, 1431 | vectors, maps and booleans are allowed. This is the equivalent of `expr` in 1432 | python. **Note that since these queries are functions and not methods, this 1433 | function is hardly ever needed since it is already implicit.** 1434 | 1435 | ```clojure 1436 | (r/parse-val [1 false "hello" :goodbye]) 1437 | ``` 1438 | 1439 | #### js 1440 | 1441 | `([js-string])` 1442 | 1443 | Create a javascript expression. 1444 | 1445 | ```clojure 1446 | (-> (r/js "1 + 1") (run conn)) 1447 | ``` 1448 | 1449 | #### coerce-to 1450 | 1451 | `([item type-string])` 1452 | 1453 | Convert a value of one type into another. 1454 | 1455 | You can convert: a selection, sequence, or object into an ARRAY, an array of pairs 1456 | into an OBJECT, and any DATUM into a STRING. 1457 | 1458 | ```clojure 1459 | (-> (r/table "marvel") 1460 | (r/coerce-to :array) 1461 | (run conn)) 1462 | ``` 1463 | 1464 | #### type 1465 | 1466 | `([item])` 1467 | 1468 | Get the type of a value. 1469 | 1470 | ```clojure 1471 | (-> (r/parse-val "hello!") 1472 | (r/type) 1473 | (run conn)) 1474 | ``` 1475 | 1476 | #### info 1477 | 1478 | `([any])` 1479 | 1480 | Get information about a rql value 1481 | 1482 | ```clojure 1483 | (-> (r/table "marvel") 1484 | (r/info) 1485 | (run conn)) 1486 | ``` 1487 | 1488 | #### json 1489 | 1490 | `([json-str])` 1491 | 1492 | Parse a JSON string on the server. 1493 | 1494 | ```clojure 1495 | (-> (r/json "[1,2,3]") (run conn)) 1496 | ``` 1497 | 1498 | ### Time constants 1499 | 1500 | Time constants are already evaluated and so they don't have to be called as fns. 1501 | 1502 | `r/monday` => 1 1503 | 1504 | `r/tuesday` => 2 1505 | 1506 | `r/wednesday` => 3 1507 | 1508 | `r/thursday` => 4 1509 | 1510 | `r/friday` => 5 1511 | 1512 | `r/saturday` => 6 1513 | 1514 | `r/sunday` => 7 1515 | 1516 | 1517 | `r/january` => 1 1518 | 1519 | `r/february` => 2 1520 | 1521 | `r/march` => 3 1522 | 1523 | `r/april` => 4 1524 | 1525 | `r/may` => 5 1526 | 1527 | `r/june` => 6 1528 | 1529 | `r/july` => 7 1530 | 1531 | `r/august` => 8 1532 | 1533 | `r/september` => 9 1534 | 1535 | `r/october` => 10 1536 | 1537 | `r/november` => 11 1538 | 1539 | `r/december` => 12 1540 | 1541 | 1542 | 1543 | ## License 1544 | 1545 | Copyright © 2013 Chris Allen, César Bolaños 1546 | 1547 | Distributed under the Eclipse Public License, the same as Clojure. 1548 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 - MANY BREAKING CHANGES 2 | * Revise supports Rethinkdb 1.12 (The Wizard of Oz) 3 | * Renamed queries table-xxx-db to table-xxx and table-xxx to table-xxx-default 4 | * Removed dependency on lein-protobuf. Moved protocol buffers to their own jar. 5 | * Added the following queries: group, sum, avg, min, max, object, upcase, downcase, 6 | split-string, index-status, index-wait, sync 7 | * Removed deprecated queries grouped-map-reduce, group-by 8 | * The result promise is now a core.async channel 9 | * Added mechanism for terms CONTINUE, STOP, WAIT_NOREPLY 10 | * Now supports success-partial queries 11 | * Removed silly `or`, `and` queries as they were duplicates. 12 | 13 | # 0.0.6 14 | * Fix missing clojure.walk require in query ns 15 | 16 | # 0.0.5 17 | * Added helper queries `or`, `and` 18 | 19 | # 0.0.4 20 | * Added connection error handling when sending queries. 21 | * The function `run` from previous versions is now `run-async`. 22 | * The actual `run` now dereferences the promise automatically with a timeout 23 | and throws when it times out or the agent dies. It now takes an optional timeout 24 | which defaults to 10000 (ms) 25 | 26 | # 0.0.3 27 | * Fixed a bug where `(group-by .. {:avg :smthing})` would return the error: 28 | `"Cannot divide by 0"` 29 | * On the same note `(group-by .. {:sum :smthing}` no longer returns 0 30 | * Revise now supports all RDB queries! 31 | -------------------------------------------------------------------------------- /connections.md: -------------------------------------------------------------------------------- 1 | Revise's connection management is a little different from how most database clients work. 2 | 3 | The send operations are messages passed to an agent, with a promise (like an MVar) kicked back to the sender. 4 | 5 | The promise gets associated with the unique response token and the async agent that serializes access to the socket sends the protobuf after putting the promise in a mapping of token -> promise. 6 | 7 | The "reader" is a future running a loop that keeps sending messages back to the agent to deliver promises and disassociate them from the "on deck" mapping, it also has a "signalling" promise to short-circuit itself. 8 | 9 | The result of running all queries is a promise and can be deref'd (the @ operator) in order to block on the result. 10 | 11 | This leads to more efficient use of the connections (you don't tie up the whole connection while waiting on a result) and makes the API async by default. 12 | 13 | As long as RethinkDB sends the results in the order they completed rather than strictly ordering by when they were received, this leads to connections not getting locked for the duration of longer queries. Agent-based serialization of operations means that there is no locking in my code. 14 | 15 | Written it in 2 hours last night after a few days of thinking about it. 16 | 17 | Some of the design could potentially commend the use of core.async, but the narrow scope of message-passing in this design didn't seem to merit it. It's not a complicated dataflow, just serialized operations against a shared resource. The semantics of how agent state works were a better fit too. 18 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to revise 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject revise "0.1.0-SNAPSHOT" 2 | :description "RethinkDB client for Clojure" 3 | :url "github.com/bitemyapp/revise/" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :main bitemyapp.revise.core 7 | :plugins [[com.jakemccrary/lein-test-refresh "0.1.2"] 8 | [lein-difftest "2.0.0"]] 9 | :test-selectors {:default (fn [_] true) ;; (complement :integration) 10 | :race-condition :race-condition 11 | :all (fn [_] true)} 12 | :dependencies [[org.clojure/clojure "1.6.0"] 13 | [org.clojure/core.async "0.1.303.0-886421-alpha"] 14 | [robert/bruce "0.7.1"] 15 | [revise/protobuf "0.8.3"] 16 | [revise/rethinkdb "1.0.2"]]) 17 | -------------------------------------------------------------------------------- /src/bitemyapp/revise/connection.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.connection 2 | "Connection shenanigans" 3 | (:refer-clojure :exclude [send]) 4 | (:import [java.io DataInputStream DataOutputStream] 5 | [java.net Socket ConnectException] 6 | [flatland.protobuf PersistentProtocolBufferMap]) 7 | (:require [flatland.protobuf.core :as pb] 8 | [bitemyapp.revise.utils.bytes :refer [to-little-endian-byte-32 9 | parse-little-endian-32 10 | concat-byte-arrays]] 11 | [bitemyapp.revise.protodefs :refer [Query Response]] 12 | [bitemyapp.revise.response :refer [inflate]] 13 | [clojure.core.async :as async :refer [chan !!]] 14 | [clojure.core.async.impl.protocols :as async-impl])) 15 | 16 | (defn close 17 | "Close the connection" 18 | ([conn] 19 | (let [c @conn 20 | out (:out c) 21 | in (:in c) 22 | socket (:socket c) 23 | reader-signal (:reader-signal c)] 24 | (deliver reader-signal :stop) 25 | (.close out) 26 | (.close in) 27 | (.close socket) 28 | true))) 29 | 30 | (defn send-number 31 | [^DataOutputStream out n] 32 | (.write out (to-little-endian-byte-32 n) 0 4)) 33 | 34 | (defn send-version 35 | "Send the version. First step when making a connection" 36 | [^DataOutputStream out] 37 | ;; fking cant figure out how to get these out of the damn protobuffer 38 | (let [v1 1063369270 39 | v2 1915781601 40 | ;v3 1601562686 ;; Not implemented yet in rdb 1.12 41 | ] 42 | (send-number out v2))) 43 | 44 | (defn send-auth-key 45 | [^DataOutputStream out auth-key] 46 | (let [c (count auth-key)] 47 | (send-number out c) 48 | (.writeChars out auth-key))) 49 | 50 | ;; TODO - not yet available in 1.12 51 | (defn send-protocol-number 52 | "Not implemented yet as of rdb 1.12" 53 | [^DataOutputStream out] 54 | (let [protobuf 656407617 55 | json 2120839367] 56 | (send-number out protobuf))) 57 | 58 | (defn read-init-response 59 | [^DataInputStream in] 60 | (let [s (StringBuilder.) 61 | x (byte-array 1)] 62 | (loop [c (.read in x)] 63 | (let [x1 (aget x 0)] 64 | (if (== 0 x1) 65 | (.toString s) 66 | (do (.append s (char x1)) 67 | (recur (.read in x)))))))) 68 | 69 | (defn dissoc-in 70 | "Dissociates an entry from a nested associative structure returning a new 71 | nested structure. keys is a sequence of keys. Any empty maps that result 72 | will not be present in the new structure." 73 | [m [k & ks :as keys]] 74 | (if ks 75 | (if-let [nextmap (get m k)] 76 | (let [newmap (dissoc-in nextmap ks)] 77 | (if (seq newmap) 78 | (assoc m k newmap) 79 | (dissoc m k))) 80 | m) 81 | (dissoc m k))) 82 | 83 | (defn socket-error? [e] 84 | (not (nil? 85 | (re-find #"No method in multimethod 'initial' for dispatch value: null" 86 | (:message (bean e)))))) 87 | 88 | (defn socket-error [cause] 89 | (Exception. (str "Connection has failed, reconnect to continue. Caused by: " cause))) 90 | 91 | (defn fetch-response 92 | [^DataInputStream in] 93 | (let [size (byte-array 4)] 94 | (.read in size 0 4) 95 | (let [size (parse-little-endian-32 size) 96 | resp (byte-array size)] 97 | (.read in resp 0 size) 98 | (pb/protobuf-load Response resp)))) 99 | 100 | (defn deliver-result [conn result] 101 | (let [token (:token result) 102 | channel (get (:waiting conn) token) 103 | success-partial (and (= :success (:type result)) 104 | (= :success-partial (:success result)))] 105 | (>!! channel result) 106 | (if success-partial ;; response is incomplete 107 | conn 108 | (do 109 | (async/close! channel) 110 | (dissoc-in conn [:waiting token]))))) 111 | 112 | (defn fail-with-error [conn error] 113 | (let [channels (vals (:waiting conn))] 114 | (doseq [c channels] 115 | (>!! c error) 116 | (async/close! c)) 117 | conn)) 118 | 119 | (defn read-into-conn [conn reader-signal] 120 | (while (not (or (agent-error conn) 121 | (and (= (when (realized? reader-signal) @reader-signal) :stop) 122 | (:inputShutdown (bean (@conn :socket)))))) 123 | (try 124 | (let [resp (fetch-response (@conn :in))] 125 | (when (= resp {}) 126 | (throw (socket-error "deserialized response was {}."))) 127 | (send-off conn deliver-result (inflate resp))) 128 | (catch Exception e 129 | (send-off conn fail-with-error e))))) 130 | 131 | (defn connect 132 | [& [conn-map]] 133 | (let [default {:host "127.0.0.1" 134 | :port 28015 135 | :token 0 136 | :auth-key ""} 137 | conn-map (merge default conn-map) 138 | token (:token conn-map) 139 | auth-key (:auth-key conn-map) 140 | socket (Socket. (:host conn-map) (:port conn-map)) 141 | out (DataOutputStream. (.getOutputStream socket)) 142 | in (DataInputStream. (.getInputStream socket)) 143 | reader-signal (promise) 144 | conn (agent {:socket socket 145 | :token token 146 | :reader-signal reader-signal 147 | :waiting {} 148 | :out out 149 | :in in})] 150 | (send-version out) 151 | (send-auth-key out auth-key) 152 | (assert (= (read-init-response in) "SUCCESS")) 153 | ;; Dunno if this is kosher. 154 | (future (read-into-conn conn reader-signal)) 155 | conn)) 156 | 157 | (defn protobuf-send 158 | "Send a protobuf to the socket's outputstream" 159 | [^DataOutputStream out ^PersistentProtocolBufferMap data] 160 | (let [msg (pb/protobuf-dump data) 161 | c (count msg) 162 | full-msg (concat-byte-arrays (to-little-endian-byte-32 c) 163 | msg)] 164 | (.write out full-msg 0 (+ 4 c)))) 165 | 166 | (defn protobuf-send-start 167 | [conn channel ^PersistentProtocolBufferMap term] 168 | (let [{:keys [out token]} conn 169 | type :START] 170 | (try 171 | (protobuf-send out (pb/protobuf Query {:query term 172 | :token token 173 | :type type})) 174 | (catch Exception e 175 | (send-off *agent* fail-with-error e))) 176 | (-> (update-in conn [:token] inc) 177 | (assoc-in [:waiting token] channel)))) 178 | 179 | (defn protobuf-send-continue 180 | [conn token] 181 | (let [{:keys [out]} conn 182 | type :CONTINUE] 183 | (try 184 | (protobuf-send out (pb/protobuf Query {:token token 185 | :type type})) 186 | (catch Exception e 187 | (send-off *agent* fail-with-error e))) 188 | conn)) 189 | 190 | (defn protobuf-send-stop 191 | [conn token] 192 | (let [{:keys [out]} conn 193 | type :CONTINUE] 194 | (try 195 | (protobuf-send out (pb/protobuf Query {:token token 196 | :type type})) 197 | (catch Exception e 198 | (send-off *agent* fail-with-error e))) 199 | conn)) 200 | 201 | (defn protobuf-send-noreply-wait 202 | [conn channel] 203 | (let [{:keys [out token]} conn 204 | type :NOREPLY_WAIT] 205 | (try 206 | (protobuf-send out (pb/protobuf Query {:token token 207 | :type type})) 208 | (catch Exception e 209 | (send-off *agent* fail-with-error e))) 210 | (-> (update-in conn [:token] inc) 211 | (assoc-in [:waiting token] channel)))) 212 | 213 | (defn send-start 214 | [^PersistentProtocolBufferMap term conn] 215 | (let [c (chan)] 216 | (send-off conn protobuf-send-start c term) 217 | c)) 218 | 219 | (defn send-continue 220 | [token conn] 221 | (when-let [c (get-in @conn [:waiting token])] 222 | (if (async-impl/closed? c) 223 | (send-off conn fail-with-error 224 | (Exception. (str "The 'waiting' channel for token '" token 225 | "' was closed prematurely!"))) 226 | (do 227 | (send-off conn protobuf-send-continue token) 228 | c)))) 229 | 230 | (defn send-stop 231 | [token conn] 232 | (when-let [c (get-in @conn [:waiting token])] 233 | (if (async-impl/closed? c) 234 | (send-off conn fail-with-error 235 | (Exception. (str "The 'waiting' channel for token '" 236 | token "' was closed prematurely!"))) 237 | (do 238 | (send-off conn protobuf-send-stop token) 239 | c)))) 240 | 241 | (defn send-wait 242 | [conn] 243 | (let [c (chan)] 244 | (send-off conn protobuf-send-noreply-wait c) 245 | c)) 246 | -------------------------------------------------------------------------------- /src/bitemyapp/revise/core.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.core 2 | "Testing stuff" 3 | (:refer-clojure :exclude [send compile]) 4 | (:require [bitemyapp.revise.connection :as conn] 5 | [bitemyapp.revise.protoengine :refer [compile-term]] 6 | [bitemyapp.revise.query :as r] 7 | [clojure.core.async :as async :refer [chan !! alts!!]] 8 | [bitemyapp.revise.utils.seq :refer [join]])) 9 | 10 | (defn run-async 11 | [q conn] 12 | (conn/send-start (compile-term q) conn)) 13 | 14 | (declare full-result) 15 | 16 | (defn run 17 | ([q conn] 18 | ;; timeout-ms defaults to 10 seconds 19 | (run q conn 10000)) 20 | ([q conn timeout] 21 | (let [error (agent-error conn)] 22 | (when error 23 | (throw error))) 24 | (let [channel (run-async q conn) 25 | error (agent-error conn)] 26 | (if error 27 | (throw error) 28 | (let [t (async/timeout timeout) 29 | [result c] (alts!! [t channel])] 30 | (cond (= c t) (throw 31 | (Exception. 32 | "Timeout on query result, did your connection fail?")) 33 | (instance? java.lang.Exception result) (throw result) 34 | :else (full-result conn result))))))) 35 | 36 | ;; TODO - replace the lazy-seq with reducers? 37 | (defn full-result 38 | [connection starting-result] 39 | (let [token (:token starting-result)] 40 | (assert token) 41 | (if (and (= :success (:type starting-result)) 42 | (= :success-partial (:success starting-result))) 43 | {:type :success 44 | :token token 45 | :success :success-lazy 46 | :response 47 | (->> 48 | ((fn step [prev] 49 | (lazy-seq 50 | (if (= :success-partial (:success prev)) 51 | (let [next ( >= 4 | not + - * / mod contains? keys 5 | merge reduce map filter mapcat 6 | distinct count empty? nth 7 | group-by type replace time 8 | min max sync]) 9 | (:require [bitemyapp.revise.utils.case :refer [snake-case-keys 10 | uppercase-keys]] 11 | clojure.walk)) 12 | 13 | (defn datum? 14 | [m] 15 | (boolean 16 | (#{:R_STR :R_NUM :R_NULL :R_BOOL :R_ARRAY :R_OBJECT} (::type m)))) 17 | 18 | (declare parse-val make-obj query) 19 | 20 | (defn parse-map 21 | "Decide if a map (not an optargs map) should be made with make-obj 22 | (it has terms inside) or is a simple datum (primitive type)" 23 | [m] 24 | (let [vs (vals m) 25 | vs (clojure.core/map parse-val vs)] 26 | (if (some (complement datum?) vs) 27 | (make-obj m) 28 | {::type :R_OBJECT 29 | ::value (clojure.core/mapv (fn [k v] 30 | {:key (name k) 31 | :val v}) 32 | ;; UUURRRRGH 33 | (clojure.core/keys m) vs)}))) 34 | 35 | (defn parse-array 36 | "Decide if an array (not an args array) should be made with make-array 37 | (it has terms inside) or is a simple datum (primitive type)" 38 | [sq] 39 | (let [xs (clojure.core/mapv parse-val sq)] 40 | (if (some (complement datum?) xs) 41 | ;; Manual invocation of query 42 | {::type :MAKE_ARRAY 43 | ::args {::type :args 44 | ::value xs}} 45 | {::type :R_ARRAY 46 | ::value xs}))) 47 | 48 | (defn parse-val 49 | [x] 50 | (letfn [(dt-map [t val] 51 | (cond 52 | (clojure.core/= :R_STR t) 53 | {::type t 54 | ::value (name val)} 55 | (clojure.core/= :R_ARRAY t) 56 | (parse-array val) 57 | (clojure.core/= :R_OBJECT t) 58 | (parse-map val) 59 | :else 60 | {::type t 61 | ::value val}))] 62 | (if (clojure.core/and (clojure.core/map? x) (::type x)) 63 | x 64 | (-> (cond (clojure.core/or (keyword? x) (string? x)) :R_STR 65 | (number? x) :R_NUM 66 | (nil? x) :R_NULL 67 | (vector? x) :R_ARRAY 68 | (clojure.core/and (clojure.core/map? x) 69 | (clojure.core/not (::type x))) :R_OBJECT 70 | (clojure.core/or (false? x) (true? x)) :R_BOOL) 71 | (dt-map x))))) 72 | 73 | ;;; ------------------------------------------------------------------------- 74 | ;;; General stuff 75 | 76 | (defn query 77 | ([type] 78 | {::type type}) 79 | ([type args] 80 | {::type type 81 | ::args {::type :args 82 | ::value (clojure.core/mapv parse-val args)}}) 83 | ([type args optargs-map] 84 | (if (seq args) 85 | {::type type 86 | ::args {::type :args 87 | ::value (clojure.core/mapv parse-val args)} 88 | ::optargs {::type :optargs 89 | ::value 90 | (zipmap (clojure.core/map name (clojure.core/keys optargs-map)) 91 | (clojure.core/map parse-val (vals optargs-map)))}} 92 | {::type type 93 | ::optargs {::type :optargs 94 | ::value 95 | (zipmap (clojure.core/map name (clojure.core/keys optargs-map)) 96 | (clojure.core/map parse-val (vals optargs-map)))}}))) 97 | 98 | ;;; ------------------------------------------------------------------------- 99 | ;;; Lambdas 100 | 101 | (defn index-args 102 | [lambda-args] 103 | (zipmap lambda-args 104 | (clojure.core/map (fn [n] 105 | {::type :var 106 | ::number (parse-val (inc n))}) 107 | (range)))) 108 | 109 | (defmacro lambda 110 | [arglist & body] 111 | (let [ret (last body) 112 | arg-replacements (index-args arglist)] 113 | `(query :FUNC [(vec (clojure.core/map inc (range ~(clojure.core/count arglist)))) 114 | ;; TODO - not the best model of scope 115 | ~(clojure.walk/postwalk-replace arg-replacements ret)]))) 116 | 117 | ;;; ------------------------------------------------------------------------- 118 | ;;; Terms 119 | 120 | ;;; -- Compound types -- 121 | (defn make-array 122 | [& xs] 123 | (query :MAKE_ARRAY xs)) 124 | 125 | (defn make-obj 126 | "Takes a map and returns an object. Useful for making maps with terms inside 127 | such as the maps returned by lambdas passed as arguments to update." 128 | [m] 129 | (query :MAKE_OBJ nil m)) 130 | 131 | (defn js 132 | ([s] (query :JAVASCRIPT [s])) 133 | ([s timeout] (query :JAVASCRIPT [s] {:timeout timeout}))) 134 | 135 | ;; TODO - not yet available in 1.12 136 | #_(defn http 137 | "Takes an HTTP URL and gets it. If the get succeeds and returns valid JSON, it 138 | is converted into a DATUM. 139 | Takes an optional map with any of the following keys: 140 | data 141 | method 142 | params 143 | header 144 | attempts 145 | redirects 146 | verify 147 | depaginate 148 | auth 149 | result_format" 150 | ([s] (query :HTTP [s])) 151 | ([s opts] (query :HTTP [s] opts))) 152 | 153 | (defn error 154 | ([] (query :ERROR)) 155 | ([s] (query :ERROR [s]))) 156 | 157 | (def implicit-var 158 | "Returns a reference to the implicit value" 159 | (query :IMPLICIT_VAR)) 160 | 161 | ;;; -- Data Operators -- 162 | (defn db 163 | [db-name] 164 | (query :DB [db-name])) 165 | 166 | (defn table-default 167 | ([table-name] 168 | (query :TABLE [table-name])) 169 | ([table-name use-outdated?] 170 | (query :TABLE [table-name] {:use_outdated use-outdated?}))) 171 | 172 | (defn table 173 | ([db table-name] 174 | (query :TABLE [db table-name])) 175 | ([db table-name use-outdated?] 176 | (query :TABLE [db table-name] {:use_outdated use-outdated?}))) 177 | 178 | (defn get 179 | [table k] 180 | (let [k (name k)] 181 | (query :GET [table k]))) 182 | 183 | (defn get-all 184 | ([table xs] 185 | (query :GET_ALL (concat [table] xs))) 186 | ([table xs index] 187 | (query :GET_ALL (concat [table] xs) {:index index}))) 188 | 189 | ;;; -- DATUM Ops -- 190 | (defn = 191 | [& args] 192 | (query :EQ args)) 193 | 194 | (defn not= 195 | [& args] 196 | (query :NE args)) 197 | 198 | (defn < 199 | [& args] 200 | (query :LT args)) 201 | 202 | (defn <= 203 | [& args] 204 | (query :LE args)) 205 | 206 | (defn > 207 | [& args] 208 | (query :GT args)) 209 | 210 | (defn >= 211 | [& args] 212 | (query :GE args)) 213 | 214 | (defn not 215 | [bool] 216 | (query :NOT [bool])) 217 | 218 | (defn + 219 | "Add two numbers or concatenate two strings" 220 | [& args] 221 | (query :ADD args)) 222 | 223 | (defn - 224 | [& args] 225 | (query :SUB args)) 226 | 227 | (defn * 228 | [& args] 229 | (query :MUL args)) 230 | 231 | ;; Weird stuff happens when we redefine / and use it from another namespace 232 | (defn div 233 | [& args] 234 | (query :DIV args)) 235 | 236 | (defn mod 237 | [n1 n2] 238 | (query :MOD [n1 n2])) 239 | 240 | ;;; -- Datum Array Ops -- 241 | (defn append 242 | [array x] 243 | (query :APPEND [array x])) 244 | 245 | (defn prepend 246 | [array x] 247 | (query :PREPEND [array x])) 248 | 249 | (defn difference 250 | [array1 array2] 251 | (query :DIFFERENCE [array1 array2])) 252 | 253 | ;;; -- Set Ops -- 254 | ;;; No actual sets on rethinkdb, only arrays 255 | (defn set-insert 256 | [array x] 257 | (query :SET_INSERT [array x])) 258 | 259 | (defn set-intersection 260 | [array1 array2] 261 | (query :SET_INTERSECTION [array1 array2])) 262 | 263 | (defn set-union 264 | [array1 array2] 265 | (query :SET_UNION [array1 array2])) 266 | 267 | (defn set-difference 268 | [array1 array2] 269 | (query :SET_DIFFERENCE [array1 array2])) 270 | 271 | (defn slice 272 | [sq n1 n2] 273 | (query :SLICE [sq n1 n2])) 274 | 275 | (defn skip 276 | [sq n] 277 | (query :SKIP [sq n])) 278 | 279 | (defn limit 280 | [sq n] 281 | (query :LIMIT [sq n])) 282 | 283 | (defn indexes-of 284 | [sq lambda1-or-x] 285 | (query :INDEXES_OF [sq lambda1-or-x])) 286 | 287 | (defn contains? 288 | [sq lambda1-or-x] 289 | (query :CONTAINS [sq lambda1-or-x])) 290 | 291 | ;;; -- Stream/Object Ops -- 292 | (defn get-field 293 | "Get a particular field from an object or map that over a sequence" 294 | [obj-or-sq s] 295 | (query :GET_FIELD [obj-or-sq s])) 296 | 297 | (defn object 298 | "Creates a javascript object from k/v pairs - consider simply using a clojure 299 | map. Usage similar to clojure.core/hash-map" 300 | [& key-vals] 301 | (query :OBJECT key-vals)) 302 | 303 | (defn keys 304 | "Return an array containing the keys of the object" 305 | [obj] 306 | (query :KEYS [obj])) 307 | 308 | (defn has-fields? 309 | "Check whether an object contains all the specified fields or filters a 310 | sequence so that al objects inside of it contain all the specified fields" 311 | [obj & pathspecs] 312 | (query :HAS_FIELDS (concat [obj] pathspecs))) 313 | 314 | (defn with-fields 315 | "(with-fields sq pathspecs..) <=> (pluck (has-fields sq pathspecs..) pathspecs..)" 316 | [sq & pathspecs] 317 | (query :HAS_FIELDS (concat [sq] pathspecs))) 318 | 319 | (defn pluck 320 | "Get a subset of an object by selecting some attributes to preserve, 321 | or map that over a sequence" 322 | [obj-or-sq & pathspecs] 323 | (query :PLUCK (concat [obj-or-sq] pathspecs))) 324 | 325 | (defn without 326 | "Get a subset of an object by selecting some attributes to discard, 327 | or map that over a sequence" 328 | [obj-or-sq & pathspecs] 329 | (query :WITHOUT (concat [obj-or-sq] pathspecs))) 330 | 331 | (defn merge 332 | "Merge objects (right-preferential)" 333 | [& objs] 334 | (query :MERGE objs)) 335 | 336 | (defn literal 337 | "Indicates to MERGE to replace the other object rather than merge it" 338 | [json] 339 | (query :LITERAL [json])) 340 | 341 | ;;; -- Sequence Ops -- 342 | (defn group 343 | "Takes a stream and partitions it into multiple groups based on the fields or 344 | functions provided. Commands chained after group will be called on each of these 345 | grouped sub-streams, producing grouped data. 346 | 347 | Examples: 348 | ;; group by a key 349 | (group tbl [:player]) 350 | ;; group by a lambda 351 | (group tbl [(lambda [game] (pluck game :player :type))]) 352 | ;; group by an index 353 | (group tbl [] :index :type)" 354 | [sq ks-or-lambda1s & {:keys [index]}] 355 | (if index 356 | (query :GROUP (concat [sq] ks-or-lambda1s) {:index index}) 357 | (query :GROUP (concat [sq] ks-or-lambda1s)))) 358 | 359 | (defn sum 360 | "Sums all the elements of a sequence. If called with a field name, sums all 361 | the values of that field in the sequence, skipping elements of the sequence that 362 | lack that field. If called with a function, calls that function on every element 363 | of the sequence and sums the results, skipping elements of the sequence where 364 | that function returns nil or a non-existence error." 365 | ([sq & [k-or-lambda1]] 366 | (if k-or-lambda1 367 | (query :SUM [sq k-or-lambda1]) 368 | (query :SUM [sq])))) 369 | 370 | (defn avg 371 | "Averages all the elements of a sequence. If called with a field name, 372 | averages all the values of that field in the sequence, skipping elements of the 373 | sequence that lack that field. If called with a function, calls that function on 374 | every element of the sequence and averages the results, skipping elements of the 375 | sequence where that function returns nil or a non-existence error." 376 | ([sq & [k-or-lambda1]] 377 | (if k-or-lambda1 378 | (query :AVG [sq k-or-lambda1]) 379 | (query :AVG [sq])))) 380 | 381 | (defn min 382 | "Finds the minimum of a sequence. If called with a field name, finds the 383 | element of that sequence with the smallest value in that field. If called with a 384 | function, calls that function on every element of the sequence and returns the 385 | element which produced the smallest value, ignoring any elements where the 386 | function returns nil or produces a non-existence error." 387 | ([sq & [k-or-lambda1]] 388 | (if k-or-lambda1 389 | (query :MIN [sq k-or-lambda1]) 390 | (query :MIN [sq])))) 391 | 392 | (defn max 393 | "Finds the maximum of a sequence. If called with a field name, finds the 394 | element of that sequence with the largest value in that field. If called with a 395 | function, calls that function on every element of the sequence and returns the 396 | element which produced the largest value, ignoring any elements where the 397 | function returns nil or produces a non-existence error." 398 | ([sq & [k-or-lambda1]] 399 | (if k-or-lambda1 400 | (query :MAX [sq k-or-lambda1]) 401 | (query :MAX [sq])))) 402 | 403 | (defn between 404 | "Get all elements of a sequence between two values" 405 | ([stream-selection lower upper] 406 | (query :BETWEEN [stream-selection lower upper])) 407 | ([stream-selection lower upper index] 408 | (query :BETWEEN [stream-selection lower upper] {:index index}))) 409 | 410 | (defn reduce 411 | ([sq lambda2] 412 | (query :REDUCE [sq lambda2])) 413 | ([sq lambda2 base] 414 | (query :REDUCE [sq lambda2] {:base base}))) 415 | 416 | (defn map 417 | [sq lambda1] 418 | (query :MAP [sq lambda1])) 419 | 420 | (defn filter 421 | "Filter a sequence with either a function or a shortcut object. 422 | The body of filter is wrapped in an implicit (default .. false) and you 423 | can change the default value by specifying the default optarg. If you 424 | make the default (error), all errors caught by default will be rethrown 425 | as if the default did not exist" 426 | ([sq lambda1-or-obj] 427 | (query :FILTER [sq lambda1-or-obj])) 428 | ([sq lambda1-or-obj default-val] 429 | (query :FILTER [sq lambda1-or-obj] {:default default-val}))) 430 | 431 | (defn mapcat 432 | "Map a function over a sequence and then concatenate the results together" 433 | [sq lambda1] 434 | (query :CONCATMAP [sq lambda1])) 435 | 436 | (defn order-by 437 | "Order a sequence based on one or more attributes" 438 | [sq & strs-or-orderings] 439 | (query :ORDERBY (concat [sq] strs-or-orderings))) 440 | 441 | (defn distinct 442 | "Get all distinct elements of a sequence (like uniq)" 443 | [sq] 444 | (query :DISTINCT [sq])) 445 | 446 | (defn count 447 | "Count the number of elements in a sequence, or only the elements that match a 448 | given filter" 449 | ([sq] 450 | (query :COUNT [sq])) 451 | ([sq lambda1-or-x] 452 | (query :COUNT [sq lambda1-or-x]))) 453 | 454 | (defn empty? 455 | [sq] 456 | (query :IS_EMPTY [sq])) 457 | 458 | (defn union 459 | "Take the union of multiple sequences 460 | (preserves duplicate elements (use distinct))" 461 | [& seqs] 462 | (query :UNION seqs)) 463 | 464 | (defn nth 465 | "Get the nth element of a sequence" 466 | [sq n] 467 | (query :NTH [sq n])) 468 | 469 | ;; Removed in version 1.12 of rethinkdb, use group, map and reduce instead 470 | #_(defn grouped-map-reduce 471 | "Takes a sequence and three functions: 472 | - a function to group the sequence by 473 | - a function to map over the groups 474 | - a reduction to apply to each of the groups" 475 | ([sq lambda1 lambda1-2 lambda2] 476 | (query :GROUPED_MAP_REDUCE [sq lambda1 lambda1-2 lambda2])) 477 | ([sq lambda1 lambda1-2 lambda2 base] 478 | (query :GROUPED_MAP_REDUCE [sq lambda1 lambda1-2 lambda2] {:base base}))) 479 | 480 | ;; Removed in version 1.12 of rethinkdb, use group instead 481 | #_(defn group-by 482 | "Groups a sequence by one or more attributes and then applies a reduction. 483 | The third argument is a special object literal giving the kind of operation 484 | to be performed and anay necessary arguments. 485 | 486 | At present group-by supports the following operations 487 | - :count - count the size of the group 488 | - {:sum attr} - sum the values of the given attribute accross the group 489 | - {:avg attr} - average the values of the given attribute accross the group" 490 | [sq array operation] 491 | (let [operation-obj 492 | (-> (if (keyword? operation) 493 | {operation operation} 494 | operation) 495 | uppercase-keys)] 496 | (query :GROUPBY [sq array operation-obj]))) 497 | 498 | (defn inner-join 499 | [sq1 sq2 lambda2] 500 | (query :INNER_JOIN [sq1 sq2 lambda2])) 501 | 502 | (defn outer-join 503 | [sq1 sq2 lambda2] 504 | (query :OUTER_JOIN [sq1 sq2 lambda2])) 505 | 506 | (defn eq-join 507 | "An inner-join that does an equality comparison on two attributes" 508 | ([sq1 str sq2] 509 | (query :EQ_JOIN [sq1 str sq2])) 510 | ([sq1 str sq2 index] 511 | (query :EQ_JOIN [sq1 str sq2] {:index index}))) 512 | 513 | (defn zip 514 | [sq] 515 | (query :ZIP [sq])) 516 | 517 | ;;; -- Array Ops -- 518 | (defn insert-at 519 | "Insert an element in to an array at a given index" 520 | [array n x] 521 | (query :INSERT_AT [array n x])) 522 | 523 | (defn delete-at 524 | "Remove an element at a given index from an array" 525 | ([array n] 526 | (query :DELETE_AT [array n])) 527 | ([array n1 n2] 528 | (query :DELETE_AT [array n1 n2]))) 529 | 530 | (defn change-at 531 | "Change the element at a given index of an array" 532 | [array n x] 533 | (query :CHANGE_AT [array n x])) 534 | 535 | (defn splice-at 536 | "Splice one array in to another array" 537 | [array1 n array2] 538 | (query :SPLICE_AT [array1 n array2])) 539 | 540 | ;;; -- Type Ops -- 541 | ;;; Figure out the name of types 542 | (defn coerce-to 543 | "Coerces a datum to a named type (eg bool)" 544 | [x type] 545 | (let [type (name type)] 546 | (query :COERCE_TO [x type]))) 547 | 548 | (defn type 549 | "Returns the named type of a datum" 550 | [x] 551 | (query :TYPEOF [x])) 552 | 553 | ;;; -- Write Ops -- 554 | (defn update 555 | "Updates all the rows in a selection. 556 | Calls its function with the row to be updated and then merges the result of 557 | that call 558 | 559 | Optargs: :non-atomic -> bool - Allow the server to run non-atomic operations 560 | :durability -> :soft or :hard - Override the table durability for this 561 | operation 562 | :return-vals -> bool - Only valid for single-row modifications. If true 563 | return the new value in :new_val and the old one in old_val" 564 | [stream-or-single-selection lambda1-or-obj 565 | & {:as optargs}] 566 | (if-not optargs 567 | (query :UPDATE [stream-or-single-selection lambda1-or-obj]) 568 | (query :UPDATE [stream-or-single-selection lambda1-or-obj] 569 | (snake-case-keys optargs)))) 570 | 571 | (defn delete 572 | "Deletes all the rows in a selection 573 | 574 | Optargs: :durability -> :hard or :soft - Override the table or query's default 575 | durability setting. 576 | :return-vals -> bool - Only valid for single row deletions. If true 577 | get the value you deleted in :old_val" 578 | [stream-or-single-selection & {:as optargs}] 579 | (if-not optargs 580 | (query :DELETE [stream-or-single-selection]) 581 | (query :DELETE [stream-or-single-selection] 582 | (snake-case-keys optargs)))) 583 | 584 | (defn replace 585 | "Replaces all the rows in a selection. Calls its function with the row to be 586 | replaced, and then discards it and stores the result of that call 587 | 588 | Optargs: :non-atomic -> bool 589 | :durability -> str 590 | :return-vals -> bool" 591 | [stream-or-single-selection lambda1 & {:as optargs}] 592 | (if-not optargs 593 | (query :REPLACE [stream-or-single-selection lambda1]) 594 | (query :REPLACE [stream-or-single-selection lambda1] 595 | (snake-case-keys optargs)))) 596 | 597 | (defn insert 598 | "Insert into a table. If upsert is true, overwrites entries with the 599 | same primary key (otherwise errors) 600 | 601 | Optargs: :upsert -> bool - If true -> overwrite the data if it already exists 602 | :durability -> :soft or :hard -> Overrule the durability with which the 603 | table was created 604 | :return-vals -> bool - Only valid for single object inserts. If true 605 | get back the row you inserted on the key :nev_val" 606 | [table obj-or-sq & {:as optargs}] 607 | (if-not optargs 608 | (query :INSERT [table obj-or-sq]) 609 | (query :INSERT [table obj-or-sq] 610 | (snake-case-keys optargs)))) 611 | 612 | ;;; -- Administrative Ops -- 613 | (defn db-create 614 | "Creates a database with a particular name" 615 | [dbname] 616 | (let [dbname (name dbname)] 617 | (query :DB_CREATE [dbname]))) 618 | 619 | (defn db-drop 620 | "Drops a database with a particular name" 621 | [dbname] 622 | (let [dbname (name dbname)] 623 | (query :DB_DROP [dbname]))) 624 | 625 | (defn db-list 626 | "Lists all the databases by name" 627 | [] 628 | (query :DB_LIST)) 629 | 630 | (defn table-create-default 631 | "Creates a table with a particular name in the default database 632 | 633 | Optargs: :datacenter str 634 | :primary-key str 635 | :cache-size number 636 | :durability str" 637 | [tname & {:as optargs}] 638 | (let [tname (name tname)] 639 | (if-not optargs 640 | (query :TABLE_CREATE [tname]) 641 | (query :TABLE_CREATE [tname] (snake-case-keys optargs))))) 642 | 643 | (defn table-create 644 | "Creates a table with a particular name in a particular database 645 | 646 | Optargs: :datacenter str 647 | :primary-key str 648 | :cache-size number 649 | :durability str" 650 | [db tname & {:as optargs}] 651 | (let [tname (name tname)] 652 | (if-not optargs 653 | (query :TABLE_CREATE [db tname]) 654 | (query :TABLE_CREATE [db tname] 655 | (snake-case-keys optargs))))) 656 | 657 | (defn table-drop-default 658 | "Drops a table with a particular name from the default database" 659 | [tname] 660 | (let [tname (name tname)] 661 | (query :TABLE_DROP [tname]))) 662 | 663 | (defn table-drop 664 | "Drops a table with a particular name from a particular database" 665 | [db tname] 666 | (let [tname (name tname)] 667 | (query :TABLE_DROP [db tname]))) 668 | 669 | (defn table-list-db 670 | "Lists all the tables in the default database" 671 | [] 672 | (query :TABLE_LIST)) 673 | 674 | (defn table-list 675 | "Lists all the tables in a particular database" 676 | [db] 677 | (query :TABLE_LIST [db])) 678 | 679 | (defn sync 680 | "Ensures that previously issued soft-durability writes are complete and 681 | written to disk" 682 | [table] 683 | (query :SYNC [table])) 684 | 685 | ;;; -- Secondary indexes Ops -- 686 | (defn index-create 687 | "Creates a new secondary index with a particular name and definition 688 | Optarg: multi -> bool" 689 | ([table idx-name lambda1] 690 | (let [idx-name (name idx-name)] 691 | (query :INDEX_CREATE [table idx-name lambda1]))) 692 | ([table idx-name lambda1 multi] 693 | (let [idx-name (name idx-name)] 694 | (query :INDEX_CREATE [table idx-name lambda1] {:multi multi})))) 695 | 696 | (defn index-drop 697 | "Drops a secondary index with a particular name from the specified table" 698 | [table idx-name] 699 | (query :INDEX_DROP [table idx-name])) 700 | 701 | (defn index-list 702 | "Lists all secondary indexes on a particular table" 703 | [table] 704 | (query :INDEX_LIST [table])) 705 | 706 | (defn index-status 707 | "Gets information about whether or not a set of indexes are ready to be 708 | accessed. Returns a list of objects (clojure maps) that look like this: 709 | {\"index\" string 710 | \"ready\" boolean 711 | \"blocks_processed\" number 712 | \"blocks-total\" number}" 713 | [table & idx-names] 714 | (query :INDEX_STATUS (concat [table] idx-names))) 715 | 716 | (defn index-wait 717 | "Blocks until a set of indexes are ready to be accessed. Returns the same 718 | values as index-status; a list of objects (clojure maps) that look like: 719 | {\"index\" string 720 | \"ready\" boolean 721 | \"blocks_processed\" number 722 | \"blocks-total\" number}" 723 | [table & idx-names] 724 | (query :INDEX_WAIT (concat [table] idx-names))) 725 | 726 | ;;; -- Control Operators -- 727 | (defn funcall 728 | "Calls a function on data" 729 | [lambda-n & xs] 730 | (query :FUNCALL (concat [lambda-n] xs))) 731 | 732 | (defn branch 733 | "An if statement" 734 | [bool then else] 735 | (query :BRANCH [bool then else])) 736 | 737 | (defn any 738 | "A short circuiting or that returns a boolean" 739 | [& bools] 740 | (query :ANY bools)) 741 | 742 | (defn all 743 | "Returns true if all of its arguments are true (short-circuits)" 744 | [& bools] 745 | (query :ALL bools)) 746 | 747 | (defn foreach 748 | "Calls its function with each entry in the sequence and executes the array of 749 | terms that function returns" 750 | [sq lambda1] 751 | (query :FOREACH [sq lambda1])) 752 | 753 | ;;; -- Special Ops -- 754 | (defn asc 755 | "Indicates to order-by that this attribute is to be sorted in ascending order" 756 | [k] 757 | (query :ASC [k])) 758 | 759 | (defn desc 760 | "Indicates to order-by that this attribute is to be sorted in descending order" 761 | [k] 762 | (query :DESC [k])) 763 | 764 | (defn info 765 | "Gets info about anything. INFO is most commonly called on tables" 766 | [x] 767 | (query :INFO [x])) 768 | 769 | (defn match 770 | "(match a b) returns a match object if the string \"a\" matches the regexp #\"b\"" 771 | [s re] 772 | (query :MATCH [s (str re)])) 773 | 774 | (defn upcase 775 | "Change a string to uppercase" 776 | [s] 777 | (query :UPCASE [s])) 778 | 779 | (defn downcase 780 | "Change a string to downcase" 781 | [s] 782 | (query :DOWNCASE [s])) 783 | 784 | (defn sample 785 | "Select a number of elements from sequence with uniform distribution" 786 | [sq n] 787 | (query :SAMPLE [sq n])) 788 | 789 | (defn default 790 | "Evaluates its first argument. If that argument returns NULL or throws an error 791 | related to the absence of an expected value, default will either return its 792 | second argument or execute it if it's a function. If the second argument is a 793 | function it will be passed either the text of the error or NULL as its argument" 794 | [to-check lambda1-or-x] 795 | (query :DEFAULT [to-check lambda1-or-x])) 796 | 797 | (defn json 798 | "Parses its first argument as a json string and returns it as a datum" 799 | [s] 800 | (query :JSON [s])) 801 | 802 | ;;; -- Date/Time Ops -- 803 | 804 | (defn iso8601 805 | "Parses its first arguments as an ISO 8601 time and returns it as a datum" 806 | [s] 807 | (query :ISO8601 [s])) 808 | 809 | (defn ->iso8601 810 | "Prints a time as an ISO 8601 time" 811 | [t] 812 | (query :TO_ISO8601 [t])) 813 | 814 | (defn epoch-time 815 | "Returns a time given seconds since epoch in UTC" 816 | [n] 817 | (query :EPOCH_TIME [n])) 818 | 819 | (defn ->epoch-time 820 | "Returns seconds since epoch in UTC given a time" 821 | [t] 822 | (query :TO_EPOCH_TIME [t])) 823 | 824 | (defn now 825 | "The time the query was received by the server" 826 | [] 827 | (query :NOW)) 828 | 829 | (defn in-timezone 830 | "Puts a time into an ISO 8601 timezone" 831 | [t s] 832 | (query :IN_TIMEZONE [t s])) 833 | 834 | (defn during 835 | "(during a b c) returns whether a is in the range [b, c) 836 | a b and c are times" 837 | [a b c] 838 | (query :DURING [a b c])) 839 | 840 | (defn date 841 | "Retrieves the date portion of a time" 842 | [t] 843 | (query :DATE [t])) 844 | 845 | (defn time-of-day 846 | "(time-of-day x) == (- (date x) x)" 847 | [t] 848 | (query :TIME_OF_DAY [t])) 849 | 850 | (defn timezone 851 | [t] 852 | (query :TIMEZONE [t])) 853 | 854 | ;;; -- Accessing time components -- 855 | (defn year 856 | [t] 857 | (query :YEAR [t])) 858 | 859 | (defn month 860 | [t] 861 | (query :MONTH [t])) 862 | 863 | (defn day 864 | [t] 865 | (query :DAY [t])) 866 | 867 | (defn day-of-week 868 | [t] 869 | (query :DAY_OF_WEEK [t])) 870 | 871 | (defn day-of-year 872 | [t] 873 | (query :DAY_OF_YEAR [t])) 874 | 875 | (defn hours 876 | [t] 877 | (query :HOURS [t])) 878 | 879 | (defn minutes 880 | [t] 881 | (query :MINUTES [t])) 882 | 883 | (defn seconds 884 | [t] 885 | (query :SECONDS [t])) 886 | 887 | ;;; -- Date construction -- 888 | ;;; Apparently timezone is obligatory contrary to what the .proto docs say 889 | (defn time 890 | "Construct a time from a date and optional timezone or a date+time and optional 891 | timezone" 892 | ([y m d] 893 | (query :TIME [y m d "+00:00"])) 894 | ([y m d tz] 895 | (query :TIME [y m d tz])) 896 | ([y m d h min s] 897 | (query :TIME [y m d h min s "+00:00"])) 898 | ([y m d h min s tz] 899 | (query :TIME [y m d h min s tz]))) 900 | 901 | ;;; -- Constants for ISO 8601 days of the week -- 902 | (def monday (query :MONDAY)) 903 | (def tuesday (query :TUESDAY)) 904 | (def wednesday (query :WEDNESDAY)) 905 | (def thursday (query :THURSDAY)) 906 | (def friday (query :FRIDAY)) 907 | (def saturday (query :SATURDAY)) 908 | (def sunday (query :SUNDAY)) 909 | 910 | ;;; -- Constants for ISO 8601 months -- 911 | (def january (query :JANUARY)) 912 | (def february (query :FEBRUARY)) 913 | (def march (query :MARCH)) 914 | (def april (query :APRIL)) 915 | (def may (query :MAY)) 916 | (def june (query :JUNE)) 917 | (def july (query :JULY)) 918 | (def august (query :AUGUST)) 919 | (def september (query :SEPTEMBER)) 920 | (def october (query :OCTOBER)) 921 | (def november (query :NOVEMBER)) 922 | (def december (query :DECEMBER)) 923 | 924 | ;;; ------------------------------------------------------------------------- 925 | ;;; Extra stuff 926 | 927 | (defn split 928 | "Returns an array of an split string 929 | (split s) splits on whitespace 930 | (split s \" \") splits on spaces only 931 | (split s \" \" 5) splits on spaces with at most 5 results 932 | (split s nil 5) splits on whitespace with at most 5 results" 933 | ([s] (query :SPLIT [s])) 934 | ([s splitter] (query :SPLIT [s splitter])) 935 | ([s splitter result-count] (query :SPLIT [s splitter result-count]))) 936 | 937 | (defn ungroup 938 | [grouped-data] 939 | (query :UNGROUP [grouped-data])) 940 | 941 | ;; TODO - not yet available in 1.12 942 | #_(defn random 943 | "Takes a range of numbers and returns a random number within the range" 944 | [from to & [float?]] 945 | (let [float? (boolean float?)]) 946 | (query :RANDOM [from to] {:float float?})) 947 | 948 | ;; TODO - not yet available in 1.12 949 | #_(defn changes 950 | [table] 951 | (query :CHANGES [table])) 952 | 953 | ;;; ------------------------------------------------------------------------- 954 | ;;; Custom Helpers 955 | -------------------------------------------------------------------------------- /src/bitemyapp/revise/response.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.response 2 | "Parsing a response from rethinkdb") 3 | 4 | (defmulti response 5 | "Inner response" 6 | (fn [x] 7 | (cond (vector? x) :vec 8 | (map? x) (:type x)))) 9 | 10 | (defmethod response 11 | :vec 12 | [v] 13 | (map response v)) 14 | 15 | ;;; On null values, usually selecting an empty table? 16 | (defmethod response 17 | :default 18 | [x] 19 | nil) 20 | 21 | (defmethod response 22 | :r-array 23 | [m] 24 | (mapv response (:r-array m))) 25 | 26 | (defmethod response 27 | :r-str 28 | [m] 29 | (:r-str m)) 30 | 31 | (defn ends-in-0 [n] 32 | (contains? #{0.0 0} (mod n 1))) 33 | 34 | (defn maybe-int [n] 35 | (or (and (ends-in-0 n) (int n)) n)) 36 | 37 | (defmethod response 38 | :r-num 39 | [m] 40 | (maybe-int (:r-num m))) 41 | 42 | (defmethod response 43 | :r-bool 44 | [m] 45 | (:r-bool m)) 46 | 47 | (defmethod response 48 | :r-object 49 | [m] 50 | (zipmap (map (comp keyword :key) (:r-object m)) 51 | (map (comp response :val) (:r-object m)))) 52 | 53 | (defmethod response 54 | :r-null 55 | [m] 56 | nil) 57 | 58 | (defmulti initial 59 | "Outer response" 60 | :type) 61 | 62 | (defmethod initial :success-atom 63 | [pb] 64 | {:token (:token pb) 65 | :response (response (:response pb)) 66 | :type :success 67 | :success :success-atom}) 68 | 69 | (defmethod initial :success-sequence 70 | [pb] 71 | {:token (:token pb) 72 | :response (response (:response pb)) 73 | :type :success 74 | :success :success-sequence}) 75 | 76 | (defmethod initial :success-partial 77 | [pb] 78 | {:token (:token pb) 79 | :response (response (:response pb)) 80 | :type :success 81 | :success :success-partial}) 82 | 83 | (defmethod initial :client-error 84 | [pb] 85 | {:error :client-error 86 | :type :error 87 | :token (:token pb) 88 | :response (response (:response pb)) 89 | :backtrace (:backtrace pb)}) 90 | 91 | (defmethod initial :runtime-error 92 | [pb] 93 | {:error :runtime-error 94 | :type :error 95 | :token (:token pb) 96 | :response (response (:response pb)) 97 | :backtrace (:backtrace pb)}) 98 | 99 | (defmethod initial :compile-error 100 | [pb] 101 | {:error :compile-error 102 | :type :error 103 | :token (:token pb) 104 | :response (response (:response pb)) 105 | :backtrace (:backtrace pb)}) 106 | 107 | (defn inflate 108 | "Deserialize the response protobuffer" 109 | [pb] 110 | (initial pb)) 111 | -------------------------------------------------------------------------------- /src/bitemyapp/revise/utils/bytes.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.utils.bytes 2 | "Byte array shenanigans" 3 | (:import [java.nio ByteOrder ByteBuffer])) 4 | 5 | (defn to-little-endian-32 6 | [n] 7 | (let [buf (ByteBuffer/allocate 4)] 8 | (doto buf 9 | (.putInt n) 10 | (.order ByteOrder/LITTLE_ENDIAN)) 11 | (.getInt buf 0))) 12 | 13 | (defn to-byte-32 14 | [n] 15 | (let [buf (ByteBuffer/allocate 4)] 16 | (.putInt buf n) 17 | (.array buf))) 18 | 19 | (defn to-little-endian-byte-32 20 | "(-> n to-little-endian-32 to-byte-32) pretty much" 21 | [n] 22 | (let [buf (ByteBuffer/allocate 4)] 23 | (doto buf 24 | (.order ByteOrder/LITTLE_ENDIAN) 25 | (.putInt n)) 26 | (.array buf))) 27 | 28 | (defn parse-little-endian-32 29 | [b] 30 | (let [buf (ByteBuffer/allocate 4)] 31 | (doto buf 32 | (.order ByteOrder/LITTLE_ENDIAN) 33 | (.put b)) 34 | (.getInt buf 0))) 35 | 36 | (defn concat-byte-arrays 37 | [a b] 38 | (let [size-a (count a) 39 | size (+ size-a (count b)) 40 | arr (byte-array size)] 41 | (dotimes [i size] 42 | (aset-byte arr i 43 | (if (> size-a i) 44 | (aget a i) 45 | (aget b (- i size-a))))) 46 | arr)) 47 | -------------------------------------------------------------------------------- /src/bitemyapp/revise/utils/case.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.utils.case 2 | "Snake case fns" 3 | (:require [clojure.string :as s])) 4 | 5 | (defprotocol IStringy 6 | (snake-case [x] "Turn into snake case string") 7 | ;; Maybe camel case here 8 | ) 9 | 10 | (defn split 11 | [s] 12 | (->> 13 | (s/split s (re-pattern "_|-| |(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z_])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])")) 14 | (filter seq) 15 | (map s/lower-case))) 16 | 17 | (extend-protocol IStringy 18 | java.lang.String 19 | (snake-case [s] 20 | (let [s (split s)] 21 | (s/join "_" s))) 22 | 23 | clojure.lang.Keyword 24 | (snake-case [k] 25 | (snake-case (name k)))) 26 | 27 | (defn lower-case 28 | [x] 29 | (if (keyword? x) 30 | (-> x (name) (s/lower-case) (keyword)) 31 | (-> x (s/lower-case)))) 32 | 33 | (defn snake-case-keys 34 | "Snake case the keys of a map" 35 | [m] 36 | (zipmap (map snake-case (keys m)) 37 | (vals m))) 38 | 39 | (defn capitalize-map 40 | [m] 41 | (zipmap (map (comp s/upper-case name) (keys m)) 42 | (map (comp s/upper-case name) (vals m)))) 43 | 44 | (defn uppercase-keys 45 | [m] 46 | (zipmap (map (comp s/upper-case name) (keys m)) 47 | (vals m))) 48 | -------------------------------------------------------------------------------- /src/bitemyapp/revise/utils/seq.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.utils.seq) 2 | 3 | ;; This doesn't really help much. We need reducers. 4 | (defn join 5 | "Lazily concatenates a sequence-of-sequences into a flat sequence. 6 | ( http://dev.clojure.org/jira/browse/CLJ-1218 )" 7 | [s] 8 | (lazy-seq (when-let [[x & xs] (seq s)] (concat x (join xs))))) 9 | -------------------------------------------------------------------------------- /test/bitemyapp/revise/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns bitemyapp.revise.core-test 2 | (:import [flatland.protobuf PersistentProtocolBufferMap]) 3 | (:require [clojure.test :refer :all] 4 | [clojure.data :refer [diff]] 5 | [robert.bruce :refer [try-try-again]] 6 | [flatland.protobuf.core :as pb] 7 | [bitemyapp.revise.connection :refer :all] 8 | [bitemyapp.revise.core :refer :all] 9 | [bitemyapp.revise.protodefs :refer [Query]] 10 | [bitemyapp.revise.protoengine :refer [compile-term]] 11 | [bitemyapp.revise.query :as r] 12 | [bitemyapp.revise.response :refer [inflate]])) 13 | 14 | (def drop-authors (-> (r/db "test") (r/table-drop "authors"))) 15 | (def create-authors (-> (r/db "test") (r/table-create "authors"))) 16 | 17 | (def authors [{:name "William Adama" :tv-show "Battlestar Galactica" 18 | :posts [{:title "Decommissioning speech", 19 | :rating 3.5 20 | :content "The Cylon War is long over..."}, 21 | {:title "We are at war", 22 | :content "Moments ago, this ship received word..."}, 23 | {:title "The new Earth", 24 | :content "The discoveries of the past few days..."}]} 25 | 26 | {:name "Laura Roslin", :tv-show "Battlestar Galactica", 27 | :posts [{:title "The oath of office", 28 | :rating 4 29 | :content "I, Laura Roslin, ..."}, 30 | {:title "They look like us", 31 | :content "The Cylons have the ability..."}]} 32 | 33 | {:name "Jean-Luc Picard", :tv-show "Star Trek TNG", 34 | :posts [{:title "Civil rights", 35 | :content "There are some words I've known since..."}]}]) 36 | 37 | (def insert-authors (-> (r/db "test") (r/table "authors") 38 | (r/insert authors))) 39 | 40 | (def filter-william (-> (r/table-default "authors") 41 | (r/filter 42 | (r/lambda [row] (r/= (r/get-field row :name) "William Adama"))))) 43 | 44 | (def dump-response (set authors)) 45 | 46 | (def william-response (set (filter #(= (:name %) "William Adama") authors))) 47 | 48 | (defn pare-down [docs] 49 | (map #(select-keys % [:name :posts :tv-show]) docs)) 50 | 51 | (defn prep-result [result] 52 | (set (pare-down (:response result)))) 53 | 54 | (defn dump-and-william [] 55 | [(future (prep-result (-> (r/table-default "authors") (run)))) (future (prep-result (run filter-william)))]) 56 | 57 | (defn test-match-results [] 58 | (let [[dump william] (dump-and-william)] 59 | (if (and (= dump-response @dump) (= william-response @william)) 60 | (throw (ex-info "I want racy results." {:type :python-exception :cause :eels})) 61 | (throw (Exception. (str "RACE CONDITION! non-match for " (diff dump-response @dump) (diff william-response @william))))))) 62 | 63 | (defn try-until-race [] 64 | (try-try-again {:sleep nil :tries 10 :catch [clojure.lang.ExceptionInfo]} test-match-results)) 65 | 66 | ;;; Order based on the README 67 | 68 | ;;; ----------------------------------------------------------------------- 69 | ;;; Manipulating databases 70 | (def create-database (r/db-create "revise_test_db")) 71 | (def drop-database (r/db-drop "revise_test_db")) 72 | (def db-list (r/db-list)) 73 | ;;; ----------------------------------------------------------------------- 74 | ;;; Manipulating tables 75 | (def create-table 76 | (-> (r/db "test") (r/table-create "revise_test1"))) 77 | (def create-table-optargs 78 | (-> (r/db "test") (r/table-create "revise_users" 79 | :primary-key :name))) 80 | (def drop-table 81 | (-> (r/db "test") (r/table-drop "revise_test1"))) 82 | 83 | (def create-index 84 | (-> (r/db "test") (r/table "revise_users") 85 | (r/index-create :email (r/lambda [user] (r/get-field user :email))))) 86 | (def create-multi-index 87 | (-> (r/db "test") (r/table "revise_users") 88 | (r/index-create :demo 89 | (r/lambda [user] 90 | [(r/get-field user :age) 91 | (r/get-field user :country)])))) 92 | (def list-index 93 | (-> (r/db "test") (r/table "revise_users") (r/index-list))) 94 | (def status-index 95 | (-> (r/db "test") (r/table "revise_users") (r/index-status :email :demo))) 96 | (def wait-index 97 | (-> (r/db "test") (r/table "revise_users") (r/index-wait :email))) 98 | (def drop-index 99 | (-> (r/db "test") (r/table "revise_users") (r/index-drop :email))) 100 | 101 | ;;; ----------------------------------------------------------------------- 102 | ;;; Writing data 103 | (def users (-> (r/db "test") (r/table "revise_users"))) 104 | (def data-multi 105 | [{:name "aa" :age 20 :country "us" :email "aa@ex.com" 106 | :gender "m" :posts ["a" "aa" "aaa"] 107 | :permission 1} 108 | {:name "bb" :age 21 :country "us" :email "bb@ex.com" 109 | :gender "f" :posts ["b" "bb"] 110 | :permission 1} 111 | {:name "cc" :age 20 :country "mx" :email "cc@ex.com" 112 | :gender "m" :posts ["c"] 113 | :permission 3} 114 | {:name "dd" :age 20 :country "ca" :email "dd@ex.com" 115 | :gender "m" :posts ["dddd"] 116 | :permission 3} 117 | {:name "ee" :age 21 :country "ca" :email "ee@ex.com" 118 | :gender "f" :posts ["e" "ee" "e" "eeeee"] 119 | :permission 2} 120 | {:name "ff" :age 22 :country "fr" :email "ff@ex.com" 121 | :gender "a" :posts [] 122 | :permission 2}]) 123 | (def data-single 124 | {:name "gg" :age 21 :country "in" :email "gg@ex.com" 125 | :gender "b" :posts [] :permission 3}) 126 | (def insert-multi 127 | (-> users (r/insert data-multi))) 128 | (def insert-single 129 | (-> users (r/insert data-single))) 130 | (def update-append 131 | (-> users (r/update {:admin false}))) 132 | (def update-lambda 133 | (-> users (r/update (r/lambda [user] 134 | {:age 135 | (r/+ 1 (r/get-field user :age))})))) 136 | 137 | (def replace-test 138 | (-> users (r/get "dd") 139 | (r/replace 140 | {:name "dd" :age 13 :country "ru" :email "hax@pwned.com" 141 | :admin true}))) 142 | (def delete 143 | (-> users (r/get "dd") 144 | (r/delete))) 145 | (def sync-table 146 | (-> users (r/sync))) 147 | ;;; ----------------------------------------------------------------------- 148 | ;;; Selecting data 149 | (def reference-db 150 | (r/db "test")) 151 | (def select-table 152 | users) 153 | (def get-doc 154 | (-> users (r/get "aa"))) 155 | (def get-all 156 | (-> users (r/get-all ["aa" "bb"]))) 157 | (def between 158 | (-> users (r/between "aa" "dd"))) 159 | (def filter-test 160 | (-> users (r/filter (r/lambda [user] (r/= 21 161 | (r/get-field user :age)))))) 162 | ;;; ----------------------------------------------------------------------- 163 | ;;; Joins 164 | (def create-permissions 165 | (-> (r/db "test") (r/table-create "revise_permissions" :primary-key :number))) 166 | (def permissions-data 167 | [{:number 1 :admin false :permission "read"} 168 | {:number 2 :admin false :permission "write"} 169 | {:number 3 :admin false :permission "execute"}]) 170 | (def permissions 171 | (-> (r/db "test") (r/table "revise_permissions"))) 172 | (def add-permissions 173 | (-> permissions (r/insert permissions-data))) 174 | (def inner-join 175 | (-> users (r/inner-join permissions 176 | (r/lambda [user perm] 177 | (r/= (r/get-field user :admin) 178 | (r/get-field perm :admin)))))) 179 | (def outer-join 180 | (-> users (r/outer-join permissions 181 | (r/lambda [user perm] 182 | (r/= (r/get-field user :admin) 183 | (r/get-field perm :admin)))))) 184 | (def eq-join 185 | (-> users (r/eq-join :permission permissions :number))) 186 | (def zip 187 | (-> eq-join r/zip)) 188 | ;;; ----------------------------------------------------------------------- 189 | ;;; Transformations 190 | (def mapped 191 | (-> users (r/map (r/lambda [user] (r/get-field user :age))))) 192 | (def with-fields 193 | (-> users (r/with-fields :email :country))) 194 | (def mapcatted 195 | (-> users (r/mapcat (r/lambda [user] 196 | (r/get-field user :posts))))) 197 | (def ordered-desc 198 | (-> users (r/order-by (r/desc :age)))) 199 | (def ordered-asc 200 | (-> users (r/order-by (r/asc :age)))) 201 | (def ordered 202 | (-> users (r/order-by :age))) 203 | (def skip 204 | (-> ordered 205 | (r/skip 2))) 206 | (def limit 207 | (-> ordered 208 | (r/limit 2))) 209 | (def slice 210 | (-> ordered 211 | (r/slice 1 3))) 212 | (def nth-item 213 | (-> ordered 214 | (r/nth 1))) 215 | (def indexes-of 216 | (r/indexes-of ["a" "b" "a" "c" "a"] "a")) 217 | (def empty-array 218 | (r/empty? [])) 219 | (def union 220 | (-> users 221 | (r/union permissions))) 222 | (def sample 223 | (-> users 224 | (r/sample 2))) 225 | ;;; ----------------------------------------------------------------------- 226 | ;;; Aggregation 227 | (def count-posts 228 | (-> users 229 | (r/map (r/lambda [user] (r/count (r/get-field user :posts)))) 230 | (r/reduce (r/lambda [acc cnt] (r/+ acc cnt)) 0))) 231 | (def distinct-array 232 | (r/distinct [1 1 2 2 3 3 4 4])) 233 | ;; Gone in 1.12 234 | #_(def grouped-map-reduce 235 | (-> users 236 | (r/grouped-map-reduce 237 | (r/lambda [user] (r/get-field user :age)) 238 | (r/lambda [user] (r/count (r/get-field user :posts))) 239 | (r/lambda [acc cnt] 240 | (r/+ acc cnt)) 241 | 0))) 242 | ;; Gone in 1.12 243 | #_(def grouped-count 244 | (-> users 245 | (r/group-by [:age] :count))) 246 | #_(def grouped-sum 247 | (-> users 248 | (r/group-by [:country] {:sum :permission}))) 249 | #_(def grouped-average 250 | (-> users 251 | (r/group-by [:country] {:avg :age}))) 252 | (def grouped-data 253 | [{:id 2, :player "Bob", :points 15, :type "ranked"}, 254 | {:id 5, :player "Alice", :points 7, :type "free"}, 255 | {:id 11, :player "Bob", :points 10, :type "free"}, 256 | {:id 12, :player "Alice", :points 2, :type "free"}]) 257 | (def grouped-key 258 | (-> grouped-data 259 | (r/group [:player]))) 260 | (def grouped-lambda 261 | (-> grouped-data 262 | (r/group [(r/lambda [game] (r/pluck game :player :type))]) 263 | (r/max :points))) 264 | (def grouped 265 | (-> users 266 | (r/group [:country]))) 267 | (def summed 268 | (-> users 269 | (r/sum :age))) 270 | (def averaged 271 | (-> [2 3 4 5 6 7 8 9 10] 272 | (r/avg))) 273 | (def minimum 274 | (-> users 275 | (r/min :age))) 276 | (def maximum 277 | (-> users 278 | (r/max :age))) 279 | (def contains 280 | (-> users 281 | (r/get "aa") 282 | (r/get-field :posts) 283 | (r/contains? "aa"))) 284 | ;;; ----------------------------------------------------------------------- 285 | ;;; Document manipulation 286 | (def pluck 287 | (-> users (r/get "aa") (r/pluck :name :age))) 288 | (def without 289 | (-> users (r/get "aa") (r/pluck :posts :country :gender :email))) 290 | (def merge-test 291 | (-> users 292 | (r/get "aa") 293 | (r/merge (-> permissions (r/limit 1))))) 294 | (def append 295 | (-> users 296 | (r/get "aa") 297 | (r/update (r/lambda [user] 298 | {:posts 299 | (r/append (r/get-field user :posts) 300 | "wheee")})))) 301 | (def prepend 302 | (-> users 303 | (r/get "aa") 304 | (r/update (r/lambda [user] 305 | {:posts 306 | (r/prepend (r/get-field user :posts) 307 | "aaaah")})))) 308 | (def difference 309 | (r/difference 310 | (-> users 311 | (r/get "aa") 312 | (r/get-field :posts)) 313 | ["a" "aa" "aaa"])) 314 | (def set-insert 315 | (r/set-insert [1 1 2] 3)) 316 | (def set-union 317 | (r/set-union [1 2 3] [2 3 4])) 318 | (def set-intersection 319 | (r/set-intersection [1 2 3] [3 4 5])) 320 | (def get-field 321 | (-> users 322 | (r/get "aa") 323 | (r/get-field :name))) 324 | (def has-fields 325 | (-> users 326 | (r/get "aa") 327 | (r/has-fields? :name :email :posts))) 328 | (def insert-at 329 | (r/insert-at [1 3 4 5] 1 2)) 330 | (def splice-at 331 | (r/splice-at [1 2 6 7] 2 [3 4 5])) 332 | (def delete-at 333 | (r/delete-at [1 2 3 4 5] 1 3)) 334 | (def change-at 335 | (r/change-at [1 2 5 4 5] 2 3)) 336 | (def keys-test 337 | (-> users 338 | (r/get "aa") 339 | (r/keys))) 340 | (def object 341 | (r/object :a 1 :b 2 :c "foo" :d ["a" 1 :b])) 342 | ;;; ----------------------------------------------------------------------- 343 | ;;; String Manipulation 344 | (def match-string 345 | (-> (r/filter ["Hello" "Also" "Goodbye"] 346 | (r/lambda [s] 347 | (r/match s #"^A"))))) 348 | (def split-string1 349 | (r/split "hello 350 | world how are you ?")) 351 | (def split-string2 352 | (r/split "hello world how are you ?" " ")) 353 | (def split-string3 354 | (r/split "h e l l o w o r l d" nil 5)) 355 | (def upcase 356 | (r/upcase "hello world")) 357 | (def downcase 358 | (r/downcase "HELLO WORLD")) 359 | ;;; ----------------------------------------------------------------------- 360 | ;;; Math and Logic 361 | (def math 362 | (r/mod 7 363 | (r/+ 1 364 | (r/* 2 365 | (r/div 4 2))))) 366 | ;; (def and-test 367 | ;; (r/and true true true)) 368 | ;; (def or-test 369 | ;; (r/or false false true)) 370 | (def =test 371 | (r/= 1 1)) 372 | (def not=test 373 | (r/not= 1 2)) 374 | (def >test 375 | (r/> 5 2)) 376 | (def >=test 377 | (r/>= 5 5)) 378 | (def (r/iso8601 "2005-10-20T03:40:05.502-06:00"))) 394 | (def in-timezone 395 | (r/in-timezone time-test "-07:00")) 396 | (def timezone 397 | (r/timezone time-test)) 398 | (def during 399 | (r/during time-test (r/time 2005 10 19 "-06:00") (r/time 2005 10 21 "-06:00"))) 400 | (def date 401 | (r/date time-test)) 402 | (def time-of-day 403 | (r/time-of-day time-test)) 404 | (def ->iso8601 405 | (r/->iso8601 time-test)) 406 | (def ->epoch-time 407 | (r/->epoch-time time-test)) 408 | ;;; ----------------------------------------------------------------------- 409 | ;;; Access time fields 410 | (def year 411 | (r/year time-test)) 412 | (def month 413 | (r/month time-test)) 414 | (def day 415 | (r/day time-test)) 416 | (def day-of-week 417 | (r/day-of-week time-test)) 418 | (def day-of-year 419 | (r/day-of-year time-test)) 420 | (def hours 421 | (r/hours time-test)) 422 | (def minutes 423 | (r/minutes time-test)) 424 | (def seconds 425 | (r/seconds time-test)) 426 | ;;; ----------------------------------------------------------------------- 427 | ;;; Control structures 428 | (def branch 429 | (r/branch true 430 | "tis true!" 431 | "tis false!")) 432 | #_(def or-test 433 | (r/or false false nil 2 false)) 434 | #_(def and-test 435 | (r/and true true "wheee!")) 436 | (def any 437 | (r/any false false false true)) 438 | (def all 439 | (r/all true true true true)) 440 | (def foreach 441 | ;; TODO 442 | ) 443 | (def error 444 | (r/error "Wheeee")) 445 | (def default 446 | (r/default nil "oooooh")) 447 | (def parse-val 448 | (r/parse-val [1 false "hello" :goodbye {:a 1}])) 449 | (def js 450 | (r/js "1 + 1")) 451 | (def coerce-to 452 | (r/coerce-to {:a 1} :array)) 453 | (def type-test 454 | (r/type [1 2 3])) 455 | (def info 456 | (r/info users)) 457 | (def json 458 | (r/json "[1,2,3]")) 459 | ;;; ----------------------------------------------------------------------- 460 | ;;; Control structures 461 | (def time-constants 462 | (r/parse-val [r/monday r/tuesday r/wednesday r/thursday r/friday 463 | r/saturday r/sunday 464 | r/january r/february r/march r/april r/may r/june r/july 465 | r/august r/september r/october r/november r/december])) 466 | 467 | ;; Testing the blocking run 468 | (deftest queries 469 | (let [conn (connect) 470 | rr (fn [term] 471 | (:response (run term conn))) 472 | er (fn [term] 473 | (:error (run term conn)))] 474 | 475 | (testing "Manipulating databases" 476 | (is (= (rr create-database) [{:created 1}])) 477 | (is (contains? (set (first (rr db-list))) 478 | "revise_test_db")) 479 | (is (= (rr drop-database) [{:dropped 1}]))) 480 | 481 | (testing "Manipulating tables" 482 | (are [x y] (= x y) 483 | (rr create-table) [{:created 1}] 484 | (rr create-table-optargs) [{:created 1}] 485 | (rr drop-table) [{:dropped 1}] 486 | (rr create-index) [{:created 1}] 487 | (rr create-multi-index) [{:created 1}] 488 | (set (first (rr list-index))) #{"demo" "email"} 489 | (:type (run status-index conn)) :success 490 | (:type (run wait-index conn 20000)) :success 491 | (rr drop-index) [{:dropped 1}])) 492 | 493 | (testing "Writing data" 494 | (are [x y] (= x y) 495 | (:inserted (first (rr insert-multi))) 6 496 | (:inserted (first (rr insert-single))) 1 497 | (:replaced (first (rr update-append))) 7 498 | (:replaced (first (rr update-lambda))) 7 499 | (:replaced (first (rr replace-test))) 1 500 | (:deleted (first (rr delete))) 1 501 | (rr sync-table) [{:synced 1}])) 502 | 503 | (testing "Selecting data" 504 | (are [x y] (= x y) 505 | (er reference-db) :runtime-error 506 | (count (rr select-table)) 6 507 | (first (rr get-doc)) {:admin false :age 21, 508 | :country "us" 509 | :email "aa@ex.com" 510 | :gender "m" :name "aa" 511 | :posts ["a" "aa" "aaa"] 512 | :permission 1} 513 | ;; TODO - Sneaky extra nesting? 514 | (count (first (rr get-all))) 2 515 | (count (rr between)) 3 516 | (count (rr filter-test)) 2 517 | (first (rr create-permissions)) {:created 1} 518 | (:inserted (first (rr add-permissions))) 3)) 519 | 520 | (testing "Joins" 521 | (are [x y] (= x y) 522 | ;; 6 x 3 = 18 - cartesian product 523 | (count (rr inner-join)) 18 524 | (count (rr outer-join)) 18 525 | (count (rr eq-join)) 6 526 | (count (rr zip)) 6)) 527 | 528 | (testing "Transformations" 529 | (are [x y] (= x y) 530 | (set (rr mapped)) #{21 22 23} 531 | (count (rr with-fields)) 6 532 | (set (rr mapcatted)) #{"aa" "bb" "ee" "aaa" 533 | "a" "b" "c" "e" "eeeee"} 534 | ;; TODO - more sneaky extra nesting 535 | (:age (ffirst (rr ordered-desc))) 23 536 | (:age (ffirst (rr ordered-asc))) 21 537 | (count (first (rr ordered))) 6 538 | (count (first (rr skip))) 4 539 | (count (first (rr limit))) 2 540 | (count (first (rr slice))) 2 541 | (:age (first (rr nth-item))) 21 542 | (first (rr indexes-of)) [0 2 4] 543 | (rr empty-array) [true] 544 | (count (rr union)) 9 545 | ;; TODO - mode sneaky nesting 546 | (count (first (rr sample))) 2)) 547 | 548 | (testing "Aggregation" 549 | (are [x y] (= x y) 550 | (first (rr count-posts)) 10 551 | (set (first (rr distinct-array))) #{1 2 3 4} 552 | #_(first (rr grouped-map-reduce)) #_[{:group 21, :reduction 4} 553 | {:group 22, :reduction 6} 554 | {:group 23, :reduction 0}] 555 | #_(first (rr grouped-count)) #_[{:group {:age 21}, :reduction 2} 556 | {:group {:age 22}, :reduction 3} 557 | {:group {:age 23}, :reduction 1}] 558 | #_(set (first (rr grouped-sum))) #_#{{:group {:country "in"}, :reduction 3} 559 | {:group {:country "mx"}, :reduction 3} 560 | {:group {:country "fr"}, :reduction 2} 561 | {:group {:country "us"}, :reduction 2} 562 | {:group {:country "ca"}, :reduction 2}} 563 | #_(set (first (rr grouped-average))) #_#{{:group {:country "ca"}, :reduction 22} 564 | {:group {:country "in"}, :reduction 22} 565 | {:group {:country "mx"}, :reduction 21} 566 | {:group {:country "fr"}, :reduction 23} 567 | {:group {:country "us"}, :reduction 21.5}} 568 | (->> (rr grouped-key) 569 | (first) (:data) 570 | (map first) 571 | (set)) #{"Alice" "Bob"} 572 | (->> (rr grouped-lambda) 573 | (first) (:data) 574 | (map first) 575 | (set)) #{{:player "Alice", :type "free"} 576 | {:player "Bob", :type "free"} 577 | {:player "Bob", :type "ranked"}} 578 | (let [grp (first (rr grouped))] 579 | [(:$reql_type$ grp) 580 | (set (map first 581 | (:data grp)))]) ["GROUPED_DATA" 582 | #{"ca" "fr" "in" "mx" "us"}] 583 | (rr summed) [131] 584 | (rr averaged) [6] 585 | (:age (first (rr minimum))) 21 586 | (:age (first (rr maximum))) 23 587 | (first (rr contains)) true)) 588 | 589 | (testing "Document Manipulation" 590 | (are [x y] (= x y) 591 | (first (rr pluck)) {:age 21, :name "aa"} 592 | (first (rr without)) {:country "us" 593 | :email "aa@ex.com" 594 | :gender "m" 595 | :posts ["a" "aa" "aaa"]} 596 | (:replaced (first (rr append))) 1 597 | (:replaced (first (rr prepend))) 1 598 | (:posts (first (rr (r/get users "aa")))) ["aaaah" "a" "aa" "aaa" 599 | "wheee"] 600 | (first (rr difference)) ["aaaah" "wheee"] 601 | (rr set-insert) [[1 2 3]] 602 | (rr set-union) [[1 2 3 4]] 603 | (rr set-intersection) [[3]] 604 | (rr get-field) ["aa"] 605 | (rr has-fields) [true] 606 | (rr insert-at) [[1 2 3 4 5]] 607 | (rr splice-at) [[1 2 3 4 5 6 7]] 608 | (rr delete-at) [[1 4 5]] 609 | (rr change-at) [[1 2 3 4 5]] 610 | (set (first (rr keys-test))) #{"gender" "name" "permission" 611 | "admin" "posts" "country" "email" "age"} 612 | (rr object) [{:a 1 :b 2 :c "foo" :d ["a" 1 "b"]}])) 613 | 614 | (testing "String Manipulation" 615 | (are [x y] (= x y) 616 | (rr match-string) [["Also"]] 617 | (rr split-string1) [["hello" "world" "how" "are" "you" "?"]] 618 | (rr split-string2) [["hello" "world" "how" "are" "you" "?"]] 619 | (rr split-string3) [["h" "e" "l" "l" "o" "w o r l d"]] 620 | (rr upcase) ["HELLO WORLD"] 621 | (rr downcase) ["hello world"])) 622 | 623 | (testing "Math and Logic" 624 | (are [x y] (= x y) 625 | (rr math) [2] 626 | (rr =test) [true] 627 | (rr not=test) [true] 628 | (rr >test) [true] 629 | (rr >=test) [true] 630 | (rr iso8601) ["2005-10-20T03:40:05.502-06:00"] 657 | ;; (rr ->epoch-time) [1129801205])) 658 | 659 | (testing "Time fields access" 660 | (are [x y] (= x y) 661 | (rr year) [2005] 662 | (rr month) [10] 663 | (rr day) [20] 664 | (rr day-of-week) [4] 665 | (rr day-of-year) [293] 666 | (rr hours) [3] 667 | (rr minutes) [40] 668 | (rr seconds) [5.502])) 669 | 670 | (testing "Control structures" 671 | (are [x y] (= x y) 672 | (rr branch) ["tis true!"] 673 | #_(rr or-test) #_[2] 674 | #_(rr and-test) #_["wheee!"] 675 | (rr any) [true] 676 | (rr all) [true] 677 | (rr error) ["Wheeee"] 678 | (rr default) ["oooooh"] 679 | ;; parse-val fails why? should it be make-array?? 680 | ; (rr parse-val) 681 | (rr js) [2] 682 | (rr coerce-to) [[["a" 1]]] 683 | (rr type-test) ["ARRAY"] 684 | (first (rr info)) {:db {:name "test", :type "DB"} 685 | :indexes ["demo"], :name "revise_users" 686 | :primary_key "name", :type "TABLE"} 687 | (rr json) [[1 2 3]])) 688 | 689 | (testing "Time constants" 690 | (is (= (first (rr time-constants)) 691 | [1 2 3 4 5 6 7 1 2 3 4 5 6 7 8 9 10 11 12]))) 692 | 693 | (testing "Cleanup" 694 | (are [x y] (= x y) 695 | (first (rr (-> (r/db "test") 696 | (r/table-drop "revise_users")))) {:dropped 1} 697 | (first (rr (-> (r/db "test") 698 | (r/table-drop "revise_permissions")))) {:dropped 1})) 699 | 700 | (close conn))) 701 | --------------------------------------------------------------------------------