├── lib └── jamm-0.2.5.jar ├── bin └── ci.sh ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── src ├── clojure │ └── clojurewerkz │ │ └── cassaforte │ │ ├── query │ │ ├── column.clj │ │ ├── types.clj │ │ ├── dsl.clj │ │ └── query_builder.clj │ │ ├── debug.clj │ │ ├── aliases.clj │ │ ├── uuids.clj │ │ ├── metrics.clj │ │ ├── utils.clj │ │ ├── conversion.clj │ │ ├── policies.clj │ │ ├── client.clj │ │ ├── cql.clj │ │ └── metadata.clj └── java │ └── com │ └── datastax │ └── driver │ └── core │ └── schemabuilder │ ├── DropKeyspace.java │ ├── AlterKeyspace.java │ ├── CreateKeyspace.java │ └── KeyspaceOptions.java ├── Makefile ├── test └── clojure │ └── clojurewerkz │ └── cassaforte │ ├── copy_table_test.clj │ ├── iterate_table_test.clj │ ├── utils_test.clj │ ├── conversion_test.clj │ ├── schema_test.clj │ ├── test_helper.clj │ ├── client_test.clj │ ├── collections_test.clj │ ├── cql_test.clj │ └── query_test.clj ├── resources └── log4j.properties.unit ├── project.clj ├── README.md ├── LICENSE-EPL1.0.txt ├── LICENSE-APL2.0.txt └── ChangeLog.md /lib/jamm-0.2.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexott/cassaforte/master/lib/jamm-0.2.5.jar -------------------------------------------------------------------------------- /bin/ci.sh: -------------------------------------------------------------------------------- 1 | make start_one_node_cluster CASSANDRA_VERSION=binary:$CASSANDRA_VERSION && \ 2 | lein all do clean, test, clean && \ 3 | make stop_cluster 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml* 6 | *.jar 7 | *.class 8 | .lein-* 9 | tmp/ 10 | examples/ 11 | doc/ 12 | .DS_Store 13 | .idea/ 14 | build/ 15 | todo* 16 | *~ 17 | \#* 18 | deploy.docs.sh 19 | .* 20 | ccm/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | before_install: 3 | - "ulimit -u 4096" 4 | script: ./bin/ci.sh 5 | env: 6 | - CASSANDRA_VERSION=2.0.17 CASSANDRA_PROTOCOL_VERSION=2 7 | - CASSANDRA_VERSION=2.1.19 CASSANDRA_PROTOCOL_VERSION=3 8 | - CASSANDRA_VERSION=2.2.11 CASSANDRA_PROTOCOL_VERSION=4 9 | - CASSANDRA_VERSION=3.0.15 CASSANDRA_PROTOCOL_VERSION=4 10 | # - CASSANDRA_VERSION=3.11.1 CASSANDRA_PROTOCOL_VERSION=4 11 | jdk: 12 | - oraclejdk8 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Pre-requisites 2 | 3 | The project uses [Leiningen 2](https://leiningen.org) and **does** require Cassandra 2.0+ to be running 4 | locally. 5 | 6 | Make sure you have Leiningen 2 installed and then run tests against all supported Clojure versions using 7 | 8 | lein all do clean, javac, test 9 | 10 | ## Pull Requests 11 | 12 | Then create a branch and make your changes on it. Once you are done with your changes and all 13 | tests pass, write a [good, detailed commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) submit a pull request on GitHub. 14 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/query/column.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.query.column) 2 | 3 | (defn write-time 4 | "Selects the write time of provided column" 5 | [column] 6 | (fn writetime-query [query-builder] 7 | (.writeTime query-builder (name column)))) 8 | 9 | (defn ttl-column 10 | "Selects the ttl of provided column." 11 | [column] 12 | (fn ttl-query [query-builder] 13 | (.ttl query-builder (name column)))) 14 | 15 | (defn distinct* 16 | "" 17 | [column] 18 | (fn distinct-query [query-builder] 19 | (.distinct (.column query-builder column)))) 20 | 21 | (defn as 22 | [wrapper alias] 23 | (fn distinct-query [query-builder] 24 | (.as (wrapper query-builder) alias))) 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CONFIG_DIR := /tmp/cassaforte-data 2 | CLUSTER_NAME := cassaforte_cluster 3 | CASSANDRA_VERSION := binary:3.4 4 | 5 | maybe_install_ccm: 6 | which ccm || test -s ~/.local/bin/ccm || pip install --user ccm 7 | 8 | prepare_tmp_dir: 9 | rm -fr $(CONFIG_DIR) ;\ 10 | mkdir -p $(CONFIG_DIR) 11 | 12 | prepare_aliases: 13 | sudo ifconfig lo0 alias 127.0.0.2 up ;\ 14 | sudo ifconfig lo0 alias 127.0.0.2 up 15 | 16 | start_one_node_cluster: maybe_install_ccm prepare_tmp_dir 17 | ccm create $(CLUSTER_NAME) -n 1 -s -i 127.0.0. -b -v $(CASSANDRA_VERSION) --config-dir=$(CONFIG_DIR) 18 | 19 | start_three_node_cluster: maybe_install_ccm prepare_tmp_dir 20 | ccm create $(CLUSTER_NAME) -n 3 -s -i 127.0.0. -b -v $(CASSANDRA_VERSION) --config-dir=$(CONFIG_DIR) 21 | 22 | .PHONY: clean 23 | stop_cluster: 24 | ps ax | grep java | grep org.apache.cassandra.service.CassandraDaemon | grep -v grep | awk '{print $$1}' | xargs kill -9 25 | 26 | .PHONY: clean 27 | clean: 28 | pip uninstall ccm 29 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/debug.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.debug 16 | "Debug utilities" 17 | (:require [clojure.stacktrace :refer :all])) 18 | 19 | (defmacro catch-exceptions 20 | "Catches driver exceptions and outputs stacktrace." 21 | [& forms] 22 | `(try 23 | (do ~@forms) 24 | (catch com.datastax.driver.core.exceptions.DriverException ire# 25 | (println (.getMessage ire#)) 26 | (print-cause-trace ire#)))) 27 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/copy_table_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.copy-table-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.client :as client] 4 | [clojurewerkz.cassaforte.test-helper :refer [*session* with-table with-temporary-keyspace]] 5 | [clojurewerkz.cassaforte.cql :refer :all] 6 | [clojure.test :refer :all] 7 | )) 8 | 9 | (use-fixtures :each with-temporary-keyspace) 10 | 11 | (deftest test-copy-table-with-natural-iteration-termination 12 | (let [n 500] 13 | (dotimes [i n] 14 | (insert *session* :users {:name (str "name_" i) :city (str "city" i) :age (int i)})) 15 | 16 | (truncate *session* :users2) 17 | (is (= 0 (perform-count *session* :users2))) 18 | (copy-table *session* :users :users2 :name identity 16384) 19 | (is (= n (perform-count *session* :users2))) 20 | 21 | (dotimes [i n] 22 | (let [k (str "name_" i) 23 | a (first (select *session* :users 24 | (where {:name k}))) 25 | b (first (select *session* :users2 26 | (where {:name k})))] 27 | (is (= a b)))))) 28 | -------------------------------------------------------------------------------- /src/java/com/datastax/driver/core/schemabuilder/DropKeyspace.java: -------------------------------------------------------------------------------- 1 | package com.datastax.driver.core.schemabuilder; 2 | 3 | /** 4 | * A built DROP KEYSPACE statement. 5 | */ 6 | public class DropKeyspace extends SchemaStatement { 7 | 8 | private final String keyspaceName; 9 | private boolean ifExists = false; 10 | 11 | public DropKeyspace(String keyspaceName) { 12 | this.keyspaceName = keyspaceName; 13 | validateNotEmpty(keyspaceName, "Keyspace name"); 14 | validateNotKeyWord(keyspaceName, String.format("The keyspace name '%s' is not allowed because it is a reserved keyword", keyspaceName)); 15 | } 16 | 17 | /** 18 | * Add the 'IF EXISTS' condition to this DROP statement. 19 | * 20 | * @return this statement. 21 | */ 22 | public DropKeyspace ifExists() { 23 | this.ifExists = true; 24 | return this; 25 | } 26 | 27 | @Override 28 | public String buildInternal() { 29 | StringBuilder dropStatement = new StringBuilder("DROP KEYSPACE "); 30 | if (ifExists) { 31 | dropStatement.append("IF EXISTS "); 32 | } 33 | dropStatement.append(keyspaceName); 34 | dropStatement.append(';'); 35 | return dropStatement.toString(); 36 | } 37 | 38 | /** 39 | * Generate a DROP TABLE statement 40 | * @return the final DROP TABLE statement 41 | */ 42 | public String build() { 43 | return this.buildInternal(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/java/com/datastax/driver/core/schemabuilder/AlterKeyspace.java: -------------------------------------------------------------------------------- 1 | package com.datastax.driver.core.schemabuilder; 2 | 3 | /** 4 | * 5 | */ 6 | public class AlterKeyspace extends SchemaStatement { 7 | 8 | private final String keyspaceName; 9 | 10 | public AlterKeyspace(String keyspaceName) { 11 | this.keyspaceName = keyspaceName; 12 | } 13 | 14 | public Options withOptions() { 15 | return new Options(this); 16 | } 17 | 18 | @Override 19 | public String getQueryString() { 20 | String built = super.getQueryString(); 21 | return built + ";"; 22 | } 23 | 24 | @Override 25 | String buildInternal() { 26 | StringBuilder alterStatement = new StringBuilder(STATEMENT_START).append("ALTER KEYSPACE "); 27 | alterStatement.append(keyspaceName); 28 | return alterStatement.toString(); 29 | } 30 | 31 | public static class Options extends KeyspaceOptions { 32 | 33 | private final SchemaStatement statement; 34 | 35 | public Options(SchemaStatement statement) { 36 | this.statement = statement; 37 | } 38 | 39 | @Override 40 | String buildInternal() { 41 | StringBuilder renderedStatement = new StringBuilder(statement.buildInternal()); 42 | renderedStatement.append(" WITH"); 43 | renderedStatement.append(super.buildInternal()); 44 | renderedStatement.append(";"); 45 | return renderedStatement.toString(); 46 | 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/aliases.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.aliases) 16 | 17 | (defn alias-var 18 | "Create a var with the supplied name in the current namespace, having the same 19 | metadata and root-binding as the supplied var." 20 | [name ^clojure.lang.Var var] 21 | (apply intern *ns* (with-meta name (merge (meta var) 22 | (meta name))) 23 | (when (.hasRoot var) [@var]))) 24 | 25 | (defmacro defalias 26 | [dst src] 27 | `(alias-var (quote ~dst) (var ~src))) 28 | 29 | (defn alias-ns 30 | "Alias all the vars from namespace to the curent namespace" 31 | [ns-name] 32 | (require ns-name) 33 | (doseq [[n v] (ns-publics (the-ns ns-name))] 34 | (alias-var n v))) 35 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/iterate_table_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.iterate-table-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.test-helper :refer [*session* with-table with-temporary-keyspace]] 4 | [clojurewerkz.cassaforte.client :as client] 5 | [clojurewerkz.cassaforte.cql :refer :all] 6 | [clojure.test :refer :all])) 7 | 8 | (use-fixtures :each with-temporary-keyspace) 9 | 10 | (deftest test-iterate-table-with-natural-iteration-termination 11 | (let [n 100000] 12 | (dotimes [i n] 13 | (insert *session* :users {:name (str "name_" i) 14 | :city (str "city" i) 15 | :age (int i)})) 16 | 17 | (let [res (group-by :name 18 | (iterate-table *session* :users :name 16384))] 19 | (dotimes [i n] 20 | (let [item (first (get res (str "name_" i)))] 21 | (is (= {:name (str "name_" i) 22 | :city (str "city" i) 23 | :age (int i)} 24 | item))))))) 25 | 26 | (deftest test-iterate-table-with-explicit-limit 27 | (let [n 100] 28 | (dotimes [i n] 29 | (insert *session* :users {:name (str "name_" i) :city (str "city" i) :age (int i)})) 30 | 31 | (let [res (group-by :name 32 | (iterate-table *session* :users :name 1024))] 33 | (dotimes [i n] 34 | (let [item (first (get res (str "name_" i)))] 35 | (is (= {:name (str "name_" i) 36 | :city (str "city" i) 37 | :age (int i)} 38 | item))))))) 39 | -------------------------------------------------------------------------------- /src/java/com/datastax/driver/core/schemabuilder/CreateKeyspace.java: -------------------------------------------------------------------------------- 1 | package com.datastax.driver.core.schemabuilder; 2 | 3 | /** 4 | * 5 | */ 6 | public class CreateKeyspace extends SchemaStatement { 7 | 8 | private final String keyspaceName; 9 | private boolean ifNotExists; 10 | 11 | public CreateKeyspace(String keyspaceName) { 12 | this.keyspaceName = keyspaceName; 13 | this.ifNotExists = false; 14 | } 15 | 16 | public CreateKeyspace ifNotExists() { 17 | this.ifNotExists = true; 18 | return this; 19 | }; 20 | 21 | public Options withOptions() { 22 | return new Options(this); 23 | } 24 | 25 | @Override 26 | public String getQueryString() { 27 | String built = super.getQueryString(); 28 | return built + ";"; 29 | } 30 | 31 | @Override 32 | String buildInternal() { 33 | StringBuilder createStatement = new StringBuilder(STATEMENT_START).append("CREATE KEYSPACE "); 34 | if (ifNotExists) { 35 | createStatement.append("IF NOT EXISTS "); 36 | } 37 | createStatement.append(keyspaceName); 38 | return createStatement.toString(); 39 | } 40 | 41 | public static class Options extends KeyspaceOptions { 42 | 43 | private final SchemaStatement statement; 44 | 45 | public Options(SchemaStatement statement) { 46 | this.statement = statement; 47 | } 48 | 49 | @Override 50 | String buildInternal() { 51 | StringBuilder renderedStatement = new StringBuilder(statement.buildInternal()); 52 | renderedStatement.append(" WITH"); 53 | renderedStatement.append(super.buildInternal()); 54 | renderedStatement.append(";"); 55 | return renderedStatement.toString(); 56 | 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/utils_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.utils-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.client :as client] 4 | [clojurewerkz.cassaforte.test-helper :as th] 5 | [clojurewerkz.cassaforte.cql :refer :all] 6 | [clojure.test :refer :all] 7 | [clojurewerkz.cassaforte.utils :refer :all])) 8 | 9 | (use-fixtures :each (fn [f] 10 | (th/with-temporary-keyspace f))) 11 | 12 | ;; (let [s (th/make-test-session)] 13 | ;; (deftest test-transform-dynamic-table 14 | ;; (create-table s :time_series 15 | ;; (column-definitions {:metric :varchar 16 | ;; :time :timestamp 17 | ;; :value_1 :varchar 18 | ;; :value_2 :varchar 19 | ;; :primary-key [:metric :time]})) 20 | 21 | ;; (insert s :time_series 22 | ;; {:metric "metric1" :time 1368395185947 :value_1 "val_1_1" :value_2 "val_1_2"}) 23 | 24 | ;; (insert s :time_series 25 | ;; {:metric "metric1" :time 1368396471276 :value_1 "val_2_1" :value_2 "val_2_2"}) 26 | 27 | ;; (let [res (transform-dynamic-table (select s :time_series) 28 | ;; :metric :time)] 29 | 30 | ;; (is (= {:value_1 "val_1_1" :value_2 "val_1_2"} 31 | ;; (get-in res ["metric1" (java.util.Date. 1368395185947)]))) 32 | ;; (is (= {:value_1 "val_2_1" :value_2 "val_2_2"} 33 | ;; (get-in res ["metric1" (java.util.Date. 1368396471276)])))) 34 | 35 | ;; (drop-table s :time_series))) 36 | -------------------------------------------------------------------------------- /resources/log4j.properties.unit: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # for production, you should probably set the root to INFO 18 | # and the pattern to %c instead of %l. (%l is slower.) 19 | 20 | # output messages into a rolling log file as well as stdout 21 | log4j.rootLogger=DEBUG,stderr,R 22 | 23 | # stderr 24 | log4j.appender.stderr=org.apache.log4j.ConsoleAppender 25 | log4j.appender.stderr.target=System.err 26 | log4j.appender.stderr.layout=org.apache.log4j.PatternLayout 27 | log4j.appender.stderr.layout.ConversionPattern=%5p %d{HH:mm:ss,SSS} %m%n 28 | log4j.appender.stderr.threshold=WARN 29 | 30 | # rolling log file 31 | log4j.appender.R=org.apache.log4j.RollingFileAppender 32 | log4j.appender.file.maxFileSize=20MB 33 | log4j.appender.file.maxBackupIndex=50 34 | log4j.appender.R.layout=org.apache.log4j.PatternLayout 35 | log4j.appender.R.layout.ConversionPattern=%5p [%t] %d{ISO8601} %F (line %L) %m%n 36 | # Edit the next line to point to your logs directory 37 | log4j.appender.R.File=build/test/logs/system.log 38 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/uuids.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.uuids 16 | "Provides utility functions for UUID generation." 17 | (:import [com.datastax.driver.core.utils UUIDs] 18 | [java.util UUID])) 19 | 20 | (defn ^UUID random 21 | "Creates a new random (version 4) UUID." 22 | [] 23 | (UUID/randomUUID)) 24 | 25 | (defn ^UUID time-based 26 | "Creates a new time-based (version 1) UUID." 27 | [] 28 | (UUIDs/timeBased)) 29 | 30 | (defn ^UUID start-of 31 | "Creates a \"fake\" time-based UUID that sorts as the smallest possible 32 | version 1 UUID generated at the provided timestamp. 33 | 34 | Timestamp must be a unix timestamp." 35 | [^long timestamp] 36 | (UUIDs/startOf timestamp)) 37 | 38 | (defn ^UUID end-of 39 | "Creates a \"fake\" time-based UUID that sorts as the biggest possible 40 | version 1 UUID generated at the provided timestamp. 41 | 42 | Timestamp must be a unix timestamp." 43 | [^long timestamp] 44 | (UUIDs/endOf timestamp)) 45 | 46 | (defn ^{:tag 'long} unix-timestamp 47 | "Return the unix timestamp contained by the provided time-based UUID." 48 | [^UUID uuid] 49 | (UUIDs/unixTimestamp uuid)) 50 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/metrics.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.metrics 16 | "Access to metrics (console, CSV, etc) for the driver" 17 | (:import [com.datastax.driver.core Session] 18 | [com.yammer.metrics.reporting ConsoleReporter CsvReporter] 19 | [com.yammer.metrics.core MetricsRegistry] 20 | [java.io File] 21 | [java.util.concurrent TimeUnit])) 22 | 23 | (defn console-reporter 24 | [^Session client] 25 | (let [registry (-> client 26 | .getCluster 27 | .getMetrics 28 | .getRegistry)] 29 | (ConsoleReporter/enable registry 1000 TimeUnit/SECONDS))) 30 | 31 | (defn csv-reporter 32 | ([^Session client] 33 | (csv-reporter client "tmp/measurements" 1 TimeUnit/SECONDS)) 34 | ([^Session client ^String dir ^long period ^TimeUnit time-unit] 35 | (let [registry (-> client 36 | (.getCluster) 37 | (.getMetrics) 38 | (.getRegistry)) 39 | f (File. dir) 40 | _ (when (not (.exists f)) (.mkdir f)) 41 | reporter (CsvReporter. registry f)] 42 | (.start reporter period time-unit) 43 | (.run reporter)))) 44 | -------------------------------------------------------------------------------- /src/java/com/datastax/driver/core/schemabuilder/KeyspaceOptions.java: -------------------------------------------------------------------------------- 1 | package com.datastax.driver.core.schemabuilder; 2 | 3 | import com.google.common.base.Optional; 4 | 5 | import java.util.Map; 6 | 7 | public class KeyspaceOptions extends SchemaStatement { 8 | 9 | private Optional> replication = Optional.absent(); 10 | private Optional durableWrites = Optional.absent(); 11 | 12 | public KeyspaceOptions() { 13 | } 14 | 15 | public KeyspaceOptions replication(Map replication) { 16 | this.replication = Optional.of(replication); 17 | return this; 18 | } 19 | 20 | public KeyspaceOptions durableWrites(boolean durableWrites){ 21 | this.durableWrites = Optional.of(durableWrites); 22 | return this; 23 | } 24 | 25 | @Override 26 | String buildInternal() { 27 | StringBuilder dropStatement = new StringBuilder(" "); 28 | 29 | boolean putSeparator = false; 30 | if (replication.isPresent()) { 31 | 32 | dropStatement.append("replication = {"); 33 | 34 | 35 | int l = replication.get().entrySet().size(); 36 | for (Map.Entry e: replication.get().entrySet()) { 37 | dropStatement.append("'" + e.getKey() + "'" + ": "); 38 | if (e.getValue() instanceof String) { 39 | dropStatement.append("'" + e.getValue() + "'"); 40 | } else { 41 | dropStatement.append(e.getValue()); 42 | } 43 | 44 | if (--l > 0) { 45 | dropStatement.append(", "); 46 | } 47 | } 48 | 49 | dropStatement.append('}'); 50 | putSeparator = true; 51 | } 52 | 53 | 54 | 55 | if (durableWrites.isPresent()) { 56 | if (putSeparator) { 57 | dropStatement.append(" AND"); 58 | } 59 | 60 | dropStatement.append(" DURABLE_WRITES = " + durableWrites.get().toString()); 61 | } 62 | 63 | 64 | return dropStatement.toString(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/query/types.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.query.types 2 | (:import [com.datastax.driver.core TupleType DataType ProtocolVersion CodecRegistry])) 3 | 4 | ;; 5 | ;; Types 6 | ;; 7 | 8 | (def primitive-types 9 | {:ascii (DataType/ascii) 10 | :bigint (DataType/bigint) 11 | :blob (DataType/blob) 12 | :boolean (DataType/cboolean) 13 | :counter (DataType/counter) 14 | :decimal (DataType/decimal) 15 | :double (DataType/cdouble) 16 | :float (DataType/cfloat) 17 | :inet (DataType/inet) 18 | :int (DataType/cint) 19 | :text (DataType/text) 20 | :timestamp (DataType/timestamp) 21 | :uuid (DataType/uuid) 22 | :varchar (DataType/varchar) 23 | :varint (DataType/varint) 24 | :timeuuid (DataType/timeuuid)}) 25 | 26 | (defn resolve-primitive-type 27 | [type-or-name] 28 | (if (keyword? type-or-name) 29 | (if-let [res (get primitive-types type-or-name)] 30 | res 31 | (throw (IllegalArgumentException. (str "Column name " 32 | (name type-or-name) 33 | " was not found, pick one of (" 34 | (clojure.string/join "," (keys primitive-types)) 35 | ")")))) 36 | type-or-name)) 37 | 38 | (defn list-type 39 | [primitive-type] 40 | (DataType/list (get primitive-types primitive-type))) 41 | 42 | (defn set-type 43 | [primitive-type] 44 | (DataType/set (get primitive-types primitive-type))) 45 | 46 | (defn map-type 47 | [key-type value-type] 48 | (DataType/map (get primitive-types key-type) 49 | (get primitive-types value-type))) 50 | 51 | ;; FIXME should be using cluster instance and cluster.metadata.newTupleType instead 52 | (defn tuple-of 53 | [^ProtocolVersion protocol-version types values] 54 | (.newValue (TupleType/of protocol-version CodecRegistry/DEFAULT_INSTANCE (into-array (map #(get primitive-types %) types))) 55 | (object-array values))) 56 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/utils.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.utils 16 | (:refer-clojure :exclude [update]) 17 | (:require [clojurewerkz.cassaforte.cql :refer :all])) 18 | 19 | (defn- group-map 20 | "Groups by and applies f to each group" 21 | [k v f] 22 | (->> v 23 | (group-by k) 24 | (map (fn [[kk vv]] 25 | [kk (f (first vv))])) 26 | (apply concat) 27 | (apply hash-map))) 28 | 29 | (defn transform-dynamic-table 30 | "Brings dynamic table to its intuitive representation 31 | 32 | Converts plain table structure: 33 | 34 | | :metric | :time | :value_1 | :value_2 | 35 | |---------+-------------------------------+----------+----------| 36 | | metric1 | Sun May 12 23:46:25 CEST 2013 | val_1_1 | val_1_2 | 37 | | metric1 | Mon May 13 00:07:51 CEST 2013 | val_2_1 | val_2_2 | 38 | 39 | To something that looks more like a tree: 40 | 41 | {\"metric1\" 42 | {#inst \"2013-05-12T21:46:25.947-00:00\" 43 | {:value_1 \"val_1_1\", :value_2 \"val_1_2\"}, 44 | #inst \"2013-05-12T22:07:51.276-00:00\" 45 | {:value_1 \"val_2_1\", :value_2 \"val_2_2\"}}} 46 | 47 | | key | row | 48 | |---------+-------------------------------------------+--------------------------------------------+ 49 | | | Sun May 12 23:46:25 CEST 2013 | Mon May 13 00:07:51 CEST 2013 | 50 | | metric1 +-------------------------------------------+--------------------------------------------+ 51 | | | value_1=val_1_1 | value_2=val_1_2 | value_1=val_2_1 | value_2=val_2_2 | 52 | |---------+-------------------------------------------+--------------------------------------------+ 53 | " 54 | [coll partition-key key-part] 55 | (into {} 56 | (for [[k v] (group-by partition-key coll)] 57 | [k 58 | (group-map key-part v 59 | #(dissoc % partition-key key-part))]))) 60 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/conversion.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.conversion 16 | (:import [com.datastax.driver.core ResultSet Host Row ColumnDefinitions ColumnDefinitions] 17 | [com.datastax.driver.core DataType DataType$Name CodecRegistry ProtocolVersion] 18 | [com.datastax.driver.core.exceptions DriverException] 19 | [java.nio ByteBuffer] 20 | [java.util Map List Set])) 21 | 22 | (defn to-clj 23 | [java-val] 24 | (cond 25 | (instance? ResultSet java-val) (into [] ;; TODO: transient? 26 | (for [^Row row java-val] 27 | (let [^ColumnDefinitions cd (.getColumnDefinitions row)] 28 | (loop [row-data {} i (int 0)] 29 | (let [^String name (.getName cd i) 30 | ^DataType data-type (.getType cd i) 31 | value (to-clj (.get row i (.codecFor CodecRegistry/DEFAULT_INSTANCE data-type)))] 32 | (if (< (inc i) (.size cd)) 33 | (recur (assoc row-data (keyword name) value) (inc i)) 34 | (assoc row-data (keyword name) value))))))) 35 | (instance? Map java-val) (let [t (transient {})] 36 | (doseq [[k v] java-val] 37 | (assoc! t k v)) 38 | (persistent! t)) 39 | (instance? Set java-val) (let [t (transient #{})] 40 | (doseq [v java-val] 41 | (conj! t v)) 42 | (persistent! t)) 43 | (instance? List java-val) (let [t (transient [])] 44 | (doseq [v java-val] 45 | (conj! t v)) 46 | (persistent! t)) 47 | (instance? Host java-val) (let [^Host host java-val] 48 | {:datacenter (.getDatacenter host) 49 | :address (.getHostAddress (.getAddress host)) 50 | :rack (.getRack host)}) 51 | :else java-val)) 52 | 53 | (defn #^bytes to-bytes 54 | [^ByteBuffer byte-buffer] 55 | (let [bytes (byte-array (.remaining byte-buffer))] 56 | (.get byte-buffer bytes 0 (count bytes)) 57 | bytes)) 58 | 59 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/query/dsl.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.query.dsl 2 | (:import [com.datastax.driver.core.querybuilder QueryBuilder])) 3 | 4 | ;; 5 | ;; SELECT Statement 6 | ;; 7 | 8 | (defn count-all 9 | [] 10 | [:what-count nil]) 11 | 12 | (defn fcall 13 | [name & args] 14 | [:what-fcall [name args]]) 15 | 16 | (defn all 17 | [] 18 | [:what-all nil]) 19 | 20 | (defn unix-timestamp-of 21 | [column-name] 22 | [:what-fcall ["unixTimestampOf" [(QueryBuilder/raw (name column-name))]]]) 23 | 24 | (defn date-of 25 | [column-name] 26 | [:what-fcall ["dateOf" [(QueryBuilder/raw (name column-name))]]]) 27 | 28 | (defn columns 29 | [& columns] 30 | [:what-columns columns]) 31 | 32 | (defn column 33 | [column & keys] 34 | [:what-column [column keys]]) 35 | 36 | (defn where 37 | [m] 38 | [:where m]) 39 | 40 | (defn order-by 41 | [& orderings] 42 | [:order orderings]) 43 | 44 | (defn limit 45 | [lim] 46 | [:limit lim]) 47 | 48 | (defn allow-filtering 49 | ([] 50 | [:filtering nil]) 51 | ([filtering] 52 | [:filtering nil])) 53 | 54 | (defn from 55 | ([table-name] 56 | [:from (name table-name)]) 57 | ([keyspace-name table-name] 58 | [:from [(name keyspace-name) (name table-name)]])) 59 | 60 | ;; 61 | ;; Insert Query 62 | ;; 63 | 64 | (defn value 65 | [key value] 66 | [:value [key value]]) 67 | 68 | (defn if-not-exists 69 | [] 70 | [:if-not-exists nil]) 71 | 72 | (defn using 73 | [& m] 74 | [:using (apply array-map m)]) 75 | 76 | ;; 77 | ;; Update Statement 78 | ;; 79 | 80 | (defn only-if 81 | [m] 82 | [:only-if m]) 83 | 84 | ;; 85 | ;; Delete Statement 86 | ;; 87 | 88 | (defn if-exists 89 | [] 90 | [:if-exists nil]) 91 | 92 | ;; 93 | ;; Alter Table 94 | ;; 95 | 96 | (defn with 97 | [options] 98 | [:with-options options]) 99 | 100 | (defn add-column 101 | [column-name column-type] 102 | [:add-column [column-name column-type]]) 103 | 104 | (defn rename-column 105 | [from to] 106 | [:rename-column [from to]]) 107 | 108 | (defn drop-column 109 | [column-name] 110 | [:drop-column column-name]) 111 | 112 | (defn alter-column 113 | [column-name column-type] 114 | [:alter-column [column-name column-type]]) 115 | 116 | (defn column-definitions 117 | [m] 118 | [:column-definitions m]) 119 | 120 | ;; 121 | ;; Index 122 | ;; 123 | 124 | (defn on-table 125 | [table-name] 126 | [:on-table (name table-name)]) 127 | (defn and-column 128 | [column-name] 129 | [:and-column (name column-name)]) 130 | (defn and-keys-of-column 131 | [column-name] 132 | [:and-keys-of-column (name column-name)]) 133 | 134 | 135 | ;; 136 | ;; 137 | ;; 138 | 139 | (defn paginate 140 | "Paginate through the collection of results 141 | 142 | Params: 143 | * `where` - where query to lock a partition key 144 | * `key` - key to paginate on 145 | * `last-key` - last seen value of the key, next chunk of results will contain all keys that follow that value 146 | * `per-page` - how many results per page" 147 | ([& {:keys [key last-key per-page where] :or {page 0}}] 148 | [:paginate [per-page (if last-key 149 | (conj (vec where) [> key last-key]) 150 | where)]])) 151 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/conversion_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.conversion-test 2 | (:import [java.util HashMap ArrayList HashSet]) 3 | (:require [clojurewerkz.cassaforte.conversion :refer :all] 4 | [clojurewerkz.cassaforte.client :as client] 5 | 6 | [clojurewerkz.cassaforte.test-helper :refer [^Session *session* with-keyspace]] 7 | [clojure.test :refer :all])) 8 | 9 | (use-fixtures :each with-keyspace) 10 | 11 | (deftest ints-and-strings 12 | (client/execute *session* "create table test1(id int primary key, val varchar)") 13 | (client/execute *session* "insert into test1(id, val) values (1, 'whatsup')") 14 | (is (= [{:id 1 :val "whatsup"}] 15 | (to-clj (client/execute *session* "select * from test1"))))) 16 | (deftest list-of-ints 17 | (client/execute *session* "create table test1(id int primary key, val list)") 18 | (client/execute *session* "insert into test1(id, val) values (1, [5, 6, 7])") 19 | (is (= [{:id 1 :val [5 6 7]}] 20 | (to-clj (client/execute *session* "select * from test1"))))) 21 | (deftest set-of-doubles 22 | (client/execute *session* "create table test1(id int primary key, val set)") 23 | (client/execute *session* "insert into test1(id, val) values (1, {5.0, 6.5, 7.314})") 24 | (is (= [{:id 1 :val #{5.0 6.5 7.314}}] 25 | (to-clj (client/execute *session* "select * from test1"))))) 26 | (deftest map-of-stuff 27 | (client/execute *session* "create table test1(id int primary key, val map)") 28 | (client/execute *session* "insert into test1(id, val) values (1, { 29 | '2015-10-10 00:00:00+0000' : '127.0.0.1', 30 | '2015-11-11 11:11:11+0000' : '8.8.8.8'})") 31 | (is (= [{:id 1 :val {#inst "2015-10-10T00:00:00.000-00:00" (java.net.InetAddress/getByName "127.0.0.1") 32 | #inst "2015-11-11T11:11:11.000-00:00" (java.net.InetAddress/getByName "8.8.8.8")}}] 33 | (to-clj (client/execute *session* "select * from test1"))))) 34 | (deftest nils-everywhere 35 | (client/execute *session* "create table test1(id int primary key, val1 int, val2 text, val3 list)") 36 | (client/execute *session* "insert into test1(id, val1, val2, val3) values (1, null, null, null)") 37 | (is (= [{:id 1 :val1 nil :val2 nil :val3 []}] 38 | (to-clj (client/execute *session* "select * from test1"))))) 39 | 40 | (deftest conversion-test 41 | (testing "Map Conversion" 42 | (let [java-map (HashMap. )] 43 | (doto java-map 44 | (.put "a" 1) 45 | (.put "b" 2) 46 | (.put "c" 3)) 47 | (is (= {"a" 1 48 | "b" 2 49 | "c" 3} 50 | (to-clj java-map))))) 51 | 52 | (testing "Empty Map Conversion" 53 | (let [java-map (HashMap. )] 54 | (is (= {} 55 | (to-clj java-map))))) 56 | 57 | (testing "List Conversion" 58 | (let [java-list (ArrayList. )] 59 | (doto java-list 60 | (.add "a") 61 | (.add "b") 62 | (.add "c")) 63 | (is (= ["a" "b" "c"] 64 | (to-clj java-list))))) 65 | 66 | (testing "Empty List Conversion" 67 | (let [java-list (ArrayList. )] 68 | (is (= [] 69 | (to-clj java-list))))) 70 | 71 | (testing "List Conversion" 72 | (let [java-list (HashSet. )] 73 | (doto java-list 74 | (.add "a") 75 | (.add "b") 76 | (.add "c")) 77 | (is (= #{"a" "b" "c"} 78 | (to-clj java-list))))) 79 | 80 | (testing "Empty List Conversion" 81 | (let [java-list (HashSet. )] 82 | (is (= #{} 83 | (to-clj java-list)))))) 84 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/query/query_builder.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.query.query-builder 2 | "Functions for building dynamic CQL queries, in case you feel 3 | that `cql` namespace is too limiting for you." 4 | (:refer-clojure :exclude [update]) 5 | (:import [com.datastax.driver.core.querybuilder QueryBuilder])) 6 | 7 | ;; 8 | ;; Query Builder helper methods 9 | ;; 10 | 11 | (defn bind-marker 12 | [name] 13 | (QueryBuilder/bindMarker name)) 14 | 15 | (defn timestamp 16 | [column-name] 17 | (QueryBuilder/timestamp column-name)) 18 | 19 | (defn token 20 | [& column-names] 21 | (QueryBuilder/token (into-array (map name column-names)))) 22 | 23 | (defn function-call ;; Maybe rename to raw-function-call? 24 | [name & args] 25 | (QueryBuilder/fcall name (object-array args))) 26 | 27 | (defn now 28 | [] 29 | (function-call "now")) 30 | 31 | (defn min-timeuuid 32 | [v] 33 | (function-call "minTimeuuid" v)) 34 | 35 | (defn max-timeuuid 36 | [v] 37 | (function-call "maxTimeuuid" v)) 38 | 39 | (defn asc 40 | [^String column-name] 41 | (QueryBuilder/asc (name column-name))) 42 | 43 | (defn desc 44 | [^String column-name] 45 | (QueryBuilder/desc (name column-name))) 46 | 47 | (defn cname 48 | [^String column-name] 49 | (QueryBuilder/column column-name)) 50 | 51 | (defn quote* 52 | [s] 53 | (QueryBuilder/quote (name s))) 54 | 55 | ;; 56 | ;; Assignments 57 | ;; 58 | 59 | (defn set-column 60 | [^String column-name column-value] 61 | (QueryBuilder/set column-name column-value)) 62 | 63 | (defn increment 64 | [] 65 | (fn [column-name] 66 | (QueryBuilder/incr column-name))) 67 | 68 | (defn increment-by 69 | [by-value] 70 | (fn [column-name] 71 | (QueryBuilder/incr column-name by-value))) 72 | 73 | (defn decrement 74 | [] 75 | (fn [column-name] 76 | (QueryBuilder/decr column-name))) 77 | 78 | (defn decrement-by 79 | [by-value] 80 | (fn [column-name] 81 | (QueryBuilder/decr column-name by-value))) 82 | 83 | (defn prepend 84 | [value] 85 | (fn [column-name] 86 | (QueryBuilder/prepend column-name value))) 87 | 88 | (defn prepend-all 89 | [values] 90 | (fn [column-name] 91 | (QueryBuilder/prependAll column-name values))) 92 | 93 | (defn append 94 | [value] 95 | (fn [column-name] 96 | (QueryBuilder/append column-name value))) 97 | 98 | (defn append-all 99 | [values] 100 | (fn [column-name] 101 | (QueryBuilder/appendAll column-name values))) 102 | 103 | (defn discard 104 | [value] 105 | (fn [column-name] 106 | (QueryBuilder/discard column-name value))) 107 | 108 | (defn discard-all 109 | [values] 110 | (fn [column-name] 111 | (QueryBuilder/discardAll column-name values))) 112 | 113 | (defn set-idx 114 | [idx value] 115 | (fn [column-name] 116 | (QueryBuilder/setIdx column-name idx value))) 117 | 118 | (defn add-tail 119 | [value] 120 | (fn [column-name] 121 | (QueryBuilder/add column-name value))) 122 | 123 | (defn add-all-tail 124 | [values] 125 | (fn [column-name] 126 | (QueryBuilder/addAll column-name values))) 127 | 128 | (defn remove-tail 129 | [value] 130 | (fn [column-name] 131 | (QueryBuilder/remove column-name value))) 132 | 133 | (defn remove-all-tail 134 | [values] 135 | (fn [column-name] 136 | (QueryBuilder/removeAll column-name values))) 137 | 138 | (defn put-value 139 | [key value] 140 | (fn [column-name] 141 | (QueryBuilder/put column-name key value))) 142 | 143 | (defn put-values 144 | [values] 145 | (fn [column-name] 146 | (QueryBuilder/putAll column-name values))) 147 | 148 | (def ? (QueryBuilder/bindMarker)) 149 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojurewerkz/cassaforte "3.0.0-alpha2-SNAPSHOT" 2 | :min-lein-version "2.5.1" 3 | :description "A Clojure client for Apache Cassandra" 4 | :url "http://clojurecassandra.info" 5 | :license {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | :dependencies [[org.clojure/clojure "1.8.0"] 8 | [com.datastax.cassandra/cassandra-driver-core "3.3.1"] 9 | [com.datastax.dse/dse-java-driver-core "1.4.1"] 10 | ] 11 | :aot [clojurewerkz.cassaforte.query] 12 | :source-paths ["src/clojure"] 13 | :test-paths ["test/clojure" "test/java"] 14 | :java-source-paths ["test/java" "src/java"] 15 | :profiles {:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 16 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 17 | :master {:dependencies [[org.clojure/clojure "1.9.0-master-SNAPSHOT"]]} 18 | :dev {:jvm-opts ["-Dlog4j.configuration=log4j.properties.unit" 19 | "-Xmx2048m" 20 | "-javaagent:lib/jamm-0.2.5.jar"] 21 | :resource-paths ["resources"] 22 | 23 | :plugins [[codox "0.8.10"] 24 | [jonase/eastwood "0.2.1"]] 25 | 26 | :dependencies [[com.codahale.metrics/metrics-core "3.0.2"] 27 | [org.xerial.snappy/snappy-java "1.1.1.6"] 28 | [org.clojure/tools.trace "0.7.8"] 29 | [clj-time "0.9.0"] 30 | 31 | ;; test/development 32 | [org.clojure/tools.namespace "0.2.10"] 33 | [org.clojure/test.check "0.7.0"] 34 | [com.gfredericks/test.chuck "0.1.17"] 35 | ]}} 36 | :aliases {"all" ["with-profile" "dev:dev,1.6:dev,1.7:dev,master"]} 37 | :test-selectors {:focus :focus 38 | :client :client 39 | :cql :cql 40 | :schema :schema 41 | :stress :stress 42 | :indexes :indexes 43 | :default (fn [m] (not (:stress m))) 44 | :ci (complement :skip-ci)} 45 | :repositories {"sonatype" {:url "http://oss.sonatype.org/content/repositories/releases" 46 | :snapshots false 47 | :releases {:checksum :fail :update :always}} 48 | "sonatype-snapshots" {:url "http://oss.sonatype.org/content/repositories/snapshots" 49 | :snapshots true 50 | :releases {:checksum :fail :update :always}}} 51 | :global-vars {*warn-on-reflection* true} 52 | :pedantic? :warn 53 | :codox {:src-dir-uri "https://github.com/clojurewerkz/cassaforte/blob/master/" 54 | :sources ["src/clojure/"] 55 | :src-linenum-anchor-prefix "L" 56 | :exclude [clojurewerkz.cassaforte.conversion 57 | clojurewerkz.cassaforte.aliases 58 | clojurewerkz.cassaforte.metrics 59 | clojurewerkz.cassaforte.debug 60 | clojurewerkz.cassaforte.bytes] 61 | :output-dir "doc/api"} 62 | ) 63 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/policies.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.policies 16 | "Consistency levels, retry policies, reconnection policies, etc" 17 | (:import [com.datastax.driver.core ConsistencyLevel] 18 | [com.datastax.driver.core.policies 19 | LoadBalancingPolicy DCAwareRoundRobinPolicy DCAwareRoundRobinPolicy$Builder RoundRobinPolicy TokenAwarePolicy 20 | LoggingRetryPolicy DefaultRetryPolicy DowngradingConsistencyRetryPolicy FallthroughRetryPolicy 21 | RetryPolicy ConstantReconnectionPolicy ExponentialReconnectionPolicy])) 22 | 23 | ;; 24 | ;; Load Balancing 25 | ;; 26 | 27 | (defn round-robin-policy 28 | "Round-robin load balancing policy. Picks nodes to execute requests on in order." 29 | [] 30 | (RoundRobinPolicy.)) 31 | 32 | (defn dc-aware-round-robin-policy 33 | "Datacenter aware load balancing policy. 34 | 35 | Like round-robin but over the nodes located in the same datacenter. 36 | Nodes from other datacenters will be tried only if all requests to local nodes fail." 37 | [^String local-dc] 38 | (-> (DCAwareRoundRobinPolicy/builder) 39 | (.withLocalDc local-dc) 40 | (.build))) 41 | 42 | (defn token-aware-policy 43 | "Takes a load balancing policy and makes it token-aware" 44 | [^LoadBalancingPolicy underlying-policy] 45 | (TokenAwarePolicy. underlying-policy)) 46 | 47 | ;; 48 | ;; Retries 49 | ;; 50 | 51 | (def retry-policies {:default (constantly DefaultRetryPolicy/INSTANCE) 52 | :downgrading-consistency (constantly DowngradingConsistencyRetryPolicy/INSTANCE) 53 | :fallthrough (constantly FallthroughRetryPolicy/INSTANCE)}) 54 | 55 | (defn retry-policy 56 | [rp] 57 | ((rp retry-policies))) 58 | 59 | (defn logging-retry-policy 60 | "A retry policy that wraps another policy, logging the decision made by its sub-policy." 61 | [^RetryPolicy policy] 62 | (LoggingRetryPolicy. policy)) 63 | 64 | ;; 65 | ;; Reconnection 66 | ;; 67 | 68 | (defn exponential-reconnection-policy 69 | "Reconnection policy that waits exponentially longer between each 70 | reconnection attempt but keeps a constant delay once a maximum delay is reached. 71 | 72 | Delays should be given in milliseconds" 73 | [base-delay-ms max-delay-ms] 74 | (ExponentialReconnectionPolicy. base-delay-ms max-delay-ms)) 75 | 76 | (defn constant-reconnection-policy 77 | "Reconnection policy that waits constantly longer between each 78 | reconnection attempt but keeps a constant delay once a maximum delay is 79 | reached. 80 | 81 | Delay should be given in milliseconds" 82 | [delay-ms] 83 | (ConstantReconnectionPolicy. delay-ms)) 84 | 85 | ;; 86 | ;; Consistency Level 87 | ;; 88 | 89 | (def consistency-levels 90 | {:any ConsistencyLevel/ANY 91 | :one ConsistencyLevel/ONE 92 | :two ConsistencyLevel/TWO 93 | :three ConsistencyLevel/THREE 94 | :quorum ConsistencyLevel/QUORUM 95 | :all ConsistencyLevel/ALL 96 | :serial ConsistencyLevel/SERIAL 97 | :local-quorum ConsistencyLevel/LOCAL_QUORUM 98 | :each-quorum ConsistencyLevel/EACH_QUORUM}) 99 | 100 | (defn consistency-level 101 | [cl] 102 | (get consistency-levels cl)) 103 | 104 | (defn resolve-consistency-level 105 | [cl] 106 | (if (= (type cl) ConsistencyLevel) 107 | cl 108 | (consistency-level cl))) 109 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/schema_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.schema-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.test-helper :refer [*session* with-keyspace]] 4 | [clojurewerkz.cassaforte.cql :refer :all] 5 | [clojurewerkz.cassaforte.metadata :as md] 6 | [clojure.test :refer :all])) 7 | 8 | (defn changes-by 9 | [f f2 n] 10 | (let [r (f2)] 11 | (f) 12 | (is (= n (- (f2) r))))) 13 | 14 | (defn changes-from-to 15 | [f f2 from to] 16 | (is (= from (f2))) 17 | (f) 18 | (is (= to (f2)))) 19 | 20 | (use-fixtures :each with-keyspace) 21 | 22 | (deftest test-create-drop-table 23 | (create-table *session* :people 24 | (column-definitions {:name :varchar 25 | :title :varchar 26 | :birth_date :timestamp 27 | :primary-key [:name]}) 28 | (if-not-exists)) 29 | 30 | (let [columns (md/columns *session* "new_cql_keyspace" "people")] 31 | (is (= "text" (get-in columns [0 :type :name]))) 32 | (is (= "timestamp" (get-in columns [1 :type :name]))) 33 | (is (= "text" (get-in columns [2 :type :name]))) 34 | (is (= 3 (count columns))))) 35 | 36 | (deftest test-create-alter-keyspace 37 | (alter-keyspace *session* "new_cql_keyspace" 38 | (with {:durable-writes false 39 | :replication (array-map "class" "NetworkTopologyStrategy" 40 | "dc1" 1)})) 41 | (let [res (md/keyspace *session* "new_cql_keyspace")] 42 | (is (= :new_cql_keyspace (:name res))) 43 | (is (= false (:durable-writes res))) 44 | )) 45 | 46 | (deftest test-create-table-with-indices 47 | (create-table *session* :people 48 | (column-definitions {:name :varchar 49 | :title :varchar 50 | :birth_date :timestamp 51 | :primary-key [:name]})) 52 | (create-index *session* :people_title 53 | (on-table :people) 54 | (and-column :title)) 55 | ;; (drop-index *session* :people_title) 56 | (drop-table *session* :people)) 57 | 58 | (deftest test-create-alter-table-add-column 59 | (create-table *session* :userstmp 60 | (column-definitions {:name :varchar 61 | :title :varchar 62 | :primary-key [:name]})) 63 | (changes-by 64 | #(alter-table *session* :userstmp 65 | (add-column :birth_date :timestamp)) 66 | #(count (md/columns *session* "new_cql_keyspace" "userstmp")) 67 | 1) 68 | (drop-table *session* :userstmp)) 69 | 70 | (deftest test-create-alter-table-rename 71 | (create-table *session* :peopletmp 72 | (column-definitions {:naome :varchar 73 | :title :varchar 74 | :primary-key [:naome]})) 75 | 76 | (changes-from-to 77 | #(alter-table *session* :peopletmp 78 | (rename-column :naome :name)) 79 | #(mapv :name (:primary-key (md/table *session* "new_cql_keyspace" "peopletmp"))) 80 | [:naome] 81 | [:name])) 82 | 83 | (deftest test-create-table-with-compound-key 84 | (create-table *session* :people 85 | (column-definitions {:first_name :varchar 86 | :last_name :varchar 87 | :city :varchar 88 | :info :text 89 | :primary-key [:first_name :last_name :city]})) 90 | (let [cfd (md/table *session* "new_cql_keyspace" "people")] 91 | (is (= [:first_name] (mapv :name (:partition-key cfd)))) 92 | (is (= [:last_name :city] (mapv :name (:clustering-columns cfd)))))) 93 | 94 | (deftest test-create-table-with-composite-parition-key 95 | (create-table *session* :people 96 | (column-definitions {:first_name :varchar 97 | :last_name :varchar 98 | :city :varchar 99 | :info :text 100 | :primary-key [[:first_name :last_name] :city]})) 101 | (let [cfd (md/table *session* "new_cql_keyspace" :people)] 102 | (is (= [:first_name :last_name] (mapv :name (:partition-key cfd)))) 103 | (is (= [:city] (mapv :name (:clustering-columns cfd)))))) 104 | 105 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/test_helper.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.test-helper 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.client :as client] 4 | [clojurewerkz.cassaforte.cql :refer :all])) 5 | 6 | (def ^:dynamic *session*) 7 | 8 | (def table-definitions 9 | {:tv_series {:series_title :varchar 10 | :episode_id :int 11 | :episode_title :text 12 | :primary-key [:series_title :episode_id]}}) 13 | 14 | (defn get-cass-proto-version [] 15 | (if-let [env-value (System/getenv "CASSANDRA_PROTOCOL_VERSION")] 16 | (Integer/parseInt env-value) 17 | 4)) 18 | 19 | (defn with-temporary-keyspace 20 | [f] 21 | (let [session (client/connect ["127.0.0.1"] {:protocol-version (get-cass-proto-version)})] 22 | (try 23 | (drop-keyspace session :new_cql_keyspace (if-exists)) 24 | 25 | (create-keyspace session "new_cql_keyspace" 26 | (with {:replication 27 | {"class" "SimpleStrategy" 28 | "replication_factor" 1 }})) 29 | 30 | (use-keyspace session :new_cql_keyspace) 31 | 32 | (create-table session :users 33 | (column-definitions {:name :varchar 34 | :age :int 35 | :city :varchar 36 | :primary-key [:name]})) 37 | 38 | ;; Same table as users, used for copying and such 39 | (create-table session :users2 40 | (column-definitions {:name :varchar 41 | :age :int 42 | :city :varchar 43 | :primary-key [:name]})) 44 | 45 | (create-table session :user_posts 46 | (column-definitions {:username :varchar 47 | :post_id :varchar 48 | :body :text 49 | :primary-key [:username :post_id]})) 50 | 51 | (create-table session :user_counters 52 | (column-definitions {:name :varchar 53 | :user_count :counter 54 | :primary-key [:name]})) 55 | 56 | (create-table session :events 57 | (column-definitions {:message :varchar 58 | :created_at :timeuuid 59 | :primary-key [:created_at]})) 60 | 61 | (create-table session :events_for_in_and_range_query 62 | (column-definitions {:message :varchar 63 | :city :varchar 64 | :created_at :timeuuid 65 | :primary-key [:message :created_at]})) 66 | 67 | (create-table session :events_by_device_id_and_date 68 | (column-definitions {:date :varchar 69 | :device_id :varchar 70 | :created_at :timeuuid 71 | :payload :text 72 | :primary-key [[:device_id :date] :created_at]})) 73 | 74 | (binding [*session* session] 75 | (f)) 76 | (drop-keyspace session :new_cql_keyspace) 77 | (catch Exception e 78 | (throw e)) 79 | (finally 80 | (client/disconnect! session))))) 81 | 82 | (defn with-keyspace 83 | [f] 84 | (let [session (client/connect ["127.0.0.1"] {:protocol-version (get-cass-proto-version)})] 85 | (try 86 | (drop-keyspace session "new_cql_keyspace" 87 | (if-exists)) 88 | (create-keyspace session "new_cql_keyspace" 89 | (with {:replication 90 | {"class" "SimpleStrategy" 91 | "replication_factor" 1 }}) 92 | (if-not-exists)) 93 | (use-keyspace session "new_cql_keyspace") 94 | 95 | (binding [*session* session] 96 | (f)) 97 | 98 | (finally 99 | (drop-keyspace session "new_cql_keyspace" 100 | (if-exists)) 101 | (client/disconnect! session))))) 102 | 103 | (defmacro with-table 104 | [table-name & body] 105 | `(do 106 | (create-table *session* ~table-name 107 | (column-definitions (get table-definitions ~table-name))) 108 | ~@body 109 | (drop-table *session* ~table-name))) 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cassaforte, a Clojure Cassandra Client 2 | 3 | Cassaforte is a small, easy to use Clojure client for Apache Cassandra (2.0+) 4 | built around CQL 3. 5 | 6 | For quickstart, please refer to our [Getting Started with Clojure and Cassandra](http://clojurecassandra.info/articles/getting_started.html) 7 | guide. 8 | 9 | ## Project Goals 10 | 11 | * Provide a Clojure-friendly, easy to use API that reflects Cassandra's data model well. Dealing with the Cassandra Thrift API quirks is counterproductive. 12 | * Be well maintained. 13 | * Be well documented. 14 | * Be well tested. 15 | * Target modern Cassandra and Clojure releases. 16 | * Integrate with libraries like Joda Time. 17 | * Support URI connections to be friendly to Heroku and other PaaS providers. 18 | 19 | 20 | 21 | ## Project Maturity 22 | 23 | Cassaforte is a moderately mature project. Started in June 2012, it 24 | has reached `1.0` in July 2013 and `2.0` in December 2014. It is 25 | known to be used by dozens of companies, small and large. 26 | 27 | Cassaforte is based on the official [DataStax Java driver for Cassandra](https://github.com/datastax/java-driver) 28 | as well as [Hayt](https://github.com/mpenet/hayt), a battle tested CQL generation DSL library. 29 | 30 | 31 | 32 | ## Dependency Information (Artifacts) 33 | 34 | Cassaforte artifacts are [released to Clojars](https://clojars.org/clojurewerkz/cassaforte). If you are using Maven, add the following repository 35 | definition to your `pom.xml`: 36 | 37 | ```xml 38 | 39 | clojars.org 40 | http://clojars.org/repo 41 | 42 | ``` 43 | 44 | ### The Most Recent Version 45 | 46 | With Leiningen: 47 | 48 | ``` clojure 49 | [clojurewerkz/cassaforte "2.0.0"] 50 | ``` 51 | 52 | With Maven: 53 | 54 | ``` xml 55 | 56 | clojurewerkz 57 | cassaforte 58 | 2.0.0 59 | 60 | ``` 61 | 62 | 63 | ## Supported Features 64 | 65 | * Connection to a single node or a cluster 66 | * _All_ CQL 3.1 operations 67 | * CQL queries, including prepared statements 68 | * Nice query DSL for Clojure 69 | * Automatic deserialization of column names and values according to the schema 70 | * TLS connections, Kerberos authentication (DataStax Enterprise) 71 | 72 | 73 | ## Supported Clojure Versions 74 | 75 | Cassaforte supports Clojure 1.6+. 76 | 77 | 78 | ## Supported Apache Cassandra Versions 79 | 80 | Cassaforte is built from the ground up for CQL. 81 | 2.0 and later versions target Cassandra 2.x. 82 | 83 | 84 | ## Documentation & Examples 85 | 86 | Please refer to our [Getting Started with Clojure and Cassandra](http://clojurecassandra.info/articles/getting_started.html) 87 | guide. 88 | 89 | [Documentation guides](http://clojurecassandra.info) are not 90 | finished and will be improved over time. 91 | 92 | [API reference](http://reference.clojurecassandra.info/) is also available. 93 | 94 | 95 | Don't hesitate to join our [mailing 96 | list](https://groups.google.com/forum/?fromgroups#!forum/clojure-cassandra) 97 | and ask questions, too! 98 | 99 | 100 | 101 | ## Community 102 | 103 | To subscribe for announcements of releases, important changes and so on, please follow 104 | [@ClojureWerkz](https://twitter.com/#!/clojurewerkz) on Twitter. 105 | 106 | 107 | ## Cassaforte Is a ClojureWerkz Project 108 | 109 | Cassaforte is part of the [group of libraries known as ClojureWerkz](http://clojurewerkz.org), together with 110 | [Monger](http://clojuremongodb.info), [Elastisch](http://clojureelasticsearch.info), [Langohr](http://clojurerabbitmq.info), 111 | [Welle](http://clojureriak.info), [Titanium](http://titanium.clojurewerkz.org) and several others. 112 | 113 | 114 | 115 | ## Continuous Integration 116 | 117 | [![Continuous Integration status](https://secure.travis-ci.org/clojurewerkz/cassaforte.svg)](http://travis-ci.org/clojurewerkz/cassaforte) 118 | [![Dependencies Status](http://jarkeeper.com/clojurewerkz/cassaforte/status.svg)](http://jarkeeper.com/clojurewerkz/cassaforte) 119 | 120 | CI is hosted by [travis-ci.org](http://travis-ci.org) 121 | 122 | 123 | ## Development 124 | 125 | Cassaforte uses [Leiningen 2](https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md). Make 126 | sure you have it installed and then run tests against all supported Clojure versions using 127 | 128 | ``` 129 | lein all test 130 | ``` 131 | 132 | Then create a branch and make your changes on it. Once you are done with your changes and all 133 | tests pass, submit a pull request on Github. 134 | 135 | 136 | 137 | ## License 138 | 139 | Copyright (C) 2012-2016 Michael S. Klishin, Alex Petrov, and the ClojureWerkz team. 140 | 141 | Double licensed under the [Eclipse Public License](http://www.eclipse.org/legal/epl-v10.html) (the same as Clojure) or 142 | the [Apache Public License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 143 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/client_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.client-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.client :as client] 4 | [clojurewerkz.cassaforte.cql :refer :all] 5 | [clojure.test :refer :all] 6 | [clojurewerkz.cassaforte.test-helper :refer [get-cass-proto-version]]) 7 | (:import [com.datastax.driver.core.policies TokenAwarePolicy RoundRobinPolicy])) 8 | 9 | 10 | (deftest ^:client test-disconnect 11 | (let [s (client/connect ["127.0.0.1"] {:protocol-version (get-cass-proto-version)}) 12 | cluster (.getCluster s)] 13 | (is (= (.isClosed s) false)) 14 | (client/disconnect s) 15 | (is (= (.isClosed s) true)) 16 | (is (= (.isClosed cluster) false)) 17 | (client/shutdown-cluster cluster) 18 | (is (= (.isClosed cluster) true)))) 19 | 20 | 21 | (deftest ^:client test-disconnect! 22 | (let [s (client/connect ["127.0.0.1"] {:protocol-version (get-cass-proto-version)}) 23 | cluster (.getCluster s)] 24 | (is (= (.isClosed s) false)) 25 | (is (= (.isClosed cluster) false)) 26 | (client/disconnect! s) 27 | (is (= (.isClosed s) true)) 28 | (is (= (.isClosed cluster) true)))) 29 | 30 | 31 | (deftest ^:client test-connect-via-uri 32 | (let [s (client/connect ["127.0.0.1"] {:protocol-version (get-cass-proto-version)})] 33 | (try 34 | (drop-keyspace s :new_cql_keyspace) 35 | (catch Exception _ nil)) 36 | (create-keyspace s "new_cql_keyspace" 37 | (with {:replication 38 | {"class" "SimpleStrategy" 39 | "replication_factor" 1 }})) 40 | (let [s2 (client/connect-with-uri "cql://127.0.0.1:9042/new_cql_keyspace" {:protocol-version (get-cass-proto-version)})] 41 | (is (= (.isClosed s2) false)) 42 | (client/disconnect s2) 43 | (is (= (.isClosed s2) true))) 44 | (client/disconnect s))) 45 | 46 | 47 | (deftest ^:client test-connect 48 | (testing "Connect without options or keyspace" 49 | (let [session (client/connect ["127.0.0.1"] {:protocol-version (get-cass-proto-version)}) 50 | cluster (.getCluster session)] 51 | (is (= false (.isClosed session))) 52 | (is (= false (.isClosed cluster))) 53 | (is (= nil (.getLoggedKeyspace session))) 54 | (is (= TokenAwarePolicy (class (.. cluster getConfiguration getPolicies getLoadBalancingPolicy)))) 55 | (client/disconnect session))) 56 | 57 | (testing "Connect with keyspace, without options" 58 | (let [session (client/connect ["127.0.0.1"] "system" {:protocol-version (get-cass-proto-version)}) 59 | cluster (.getCluster session)] 60 | (is (= false (.isClosed session))) 61 | (is (= false (.isClosed cluster))) 62 | (is (= "system" (.getLoggedKeyspace session))) 63 | (is (= TokenAwarePolicy (class (.. cluster getConfiguration getPolicies getLoadBalancingPolicy)))) 64 | (client/disconnect session))) 65 | 66 | (testing "Connect with keyspace in options" 67 | (let [session (client/connect ["127.0.0.1"] {:keyspace "system" :protocol-version (get-cass-proto-version)}) 68 | cluster (.getCluster session)] 69 | (is (= false (.isClosed session))) 70 | (is (= false (.isClosed cluster))) 71 | (is (= "system" (.getLoggedKeyspace session))) 72 | (is (= TokenAwarePolicy (class (.. cluster getConfiguration getPolicies getLoadBalancingPolicy)))) 73 | (client/disconnect session))) 74 | 75 | (testing "Connect with options" 76 | (let [session (client/connect ["127.0.0.1"] {:load-balancing-policy (RoundRobinPolicy.) :protocol-version (get-cass-proto-version)}) 77 | cluster (.getCluster session)] 78 | (is (= false (.isClosed session))) 79 | (is (= false (.isClosed cluster))) 80 | (is (= nil (.getLoggedKeyspace session))) 81 | (is (= RoundRobinPolicy (class (.. cluster getConfiguration getPolicies getLoadBalancingPolicy)))) 82 | (client/disconnect session))) 83 | 84 | (testing "Connect with options and keyspace (in options)" 85 | (let [session (client/connect ["127.0.0.1"] {:load-balancing-policy (RoundRobinPolicy.) :keyspace "system" 86 | :protocol-version (get-cass-proto-version)}) 87 | cluster (.getCluster session)] 88 | (is (= false (.isClosed session))) 89 | (is (= false (.isClosed cluster))) 90 | (is (= "system" (.getLoggedKeyspace session))) 91 | (is (= RoundRobinPolicy (class (.. cluster getConfiguration getPolicies getLoadBalancingPolicy)))) 92 | (client/disconnect session))) 93 | 94 | (testing "Connect with options and keyspace (separate args)" 95 | (let [session (client/connect ["127.0.0.1"] "system" {:load-balancing-policy (RoundRobinPolicy.) 96 | :protocol-version (get-cass-proto-version)}) 97 | cluster (.getCluster session)] 98 | (is (= false (.isClosed session))) 99 | (is (= false (.isClosed cluster))) 100 | (is (= "system" (.getLoggedKeyspace session))) 101 | (is (= RoundRobinPolicy (class (.. cluster getConfiguration getPolicies getLoadBalancingPolicy)))) 102 | (client/disconnect session)))) 103 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/collections_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.collections-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.test-helper :as th] 4 | [clojurewerkz.cassaforte.client :as client] 5 | [clojurewerkz.cassaforte.cql :as cql :refer :all] 6 | [clojure.test :refer :all] 7 | )) 8 | 9 | (use-fixtures :each (fn [f] 10 | (th/with-temporary-keyspace f))) 11 | 12 | ;; (let [s (th/make-test-session)] 13 | ;; (deftest test-collection-conversion-on-load 14 | ;; (let [t :users_collections] 15 | ;; (create-table s t 16 | ;; (column-definitions 17 | ;; {:name :varchar 18 | ;; :test_map (map-type :varchar :varchar) 19 | ;; :test_set (set-type :int) 20 | ;; :test_list (list-type :varchar) 21 | ;; :primary-key [:name]})) 22 | 23 | ;; (insert s t 24 | ;; {:name "user1" 25 | ;; :test_map {"a" "b" "c" "d"} 26 | ;; :test_set #{1 2 3} 27 | ;; :test_list ["clojure" "cassandra"]}) 28 | ;; (let [m (-> (select s t) first) 29 | ;; mv (:test_map m) 30 | ;; ms (:test_set m) 31 | ;; ml (:test_list m)] 32 | ;; (is (map? mv)) 33 | ;; (is (= {"a" "b" "c" "d"} mv)) 34 | ;; (is (set? ms)) 35 | ;; (is (= #{1 2 3} ms)) 36 | ;; (is (vector? ml)) 37 | ;; (is (= ["clojure" "cassandra"] ml))) 38 | ;; (truncate s t) 39 | ;; (drop-table s t))) 40 | 41 | 42 | ;; (deftest test-list-operations 43 | ;; (create-table s :users_list 44 | ;; (column-definitions 45 | ;; {:name :varchar 46 | ;; :test_list (list-type :varchar) 47 | ;; :primary-key [:name]})) 48 | 49 | ;; (testing "Inserting" 50 | ;; (insert s :users_list 51 | ;; {:name "user1" 52 | ;; :test_list ["str1" "str2" "str3"]}) 53 | ;; (is (= ["str1" "str2" "str3"] (get-in (select s :users_list) 54 | ;; [0 :test_list]))) 55 | ;; (truncate s :users_list)) 56 | 57 | ;; (testing "Updating" 58 | ;; (insert s :users_list 59 | ;; {:name "user1" 60 | ;; :test_list []}) 61 | ;; (dotimes [i 3] 62 | ;; (update s :users_list 63 | ;; {:test_list [+ [(str "str" i)]]} 64 | ;; (where {:name "user1"}))) 65 | 66 | ;; (is (= ["str0" "str1" "str2"] (get-in (select s :users_list) 67 | ;; [0 :test_list]))) 68 | ;; (truncate s :users_list)) 69 | 70 | ;; (testing "Deleting" 71 | ;; (insert s :users_list 72 | ;; {:name "user1" 73 | ;; :test_list ["str0" "str1" "str2"]}) 74 | ;; (update s :users_list 75 | ;; {:test_list [- ["str0" "str1"]]} 76 | ;; (where {:name "user1"})) 77 | 78 | ;; (is (= ["str2"] (get-in (select s :users_list) 79 | ;; [0 :test_list])))) 80 | 81 | ;; (drop-table s :users_list)) 82 | 83 | ;; (deftest test-map-operations 84 | ;; (create-table s :users_map 85 | ;; (column-definitions 86 | ;; {:name :varchar 87 | ;; :test_map (map-type :varchar :varchar) 88 | ;; :primary-key [:name]})) 89 | 90 | ;; (testing "Inserting" 91 | ;; (insert s :users_map 92 | ;; {:name "user1" 93 | ;; :test_map {"a" "b" "c" "d"}}) 94 | ;; (is (= {"a" "b" "c" "d"} (get-in (select s :users_map) 95 | ;; [0 :test_map]))) 96 | ;; (truncate s :users_map)) 97 | 98 | ;; (testing "Updating" 99 | ;; (insert s :users_map 100 | ;; {:name "user1" 101 | ;; :test_map {}}) 102 | ;; (dotimes [i 3] 103 | ;; (update s :users_map 104 | ;; {:test_map [+ {"a" "b" "c" "d"}]} 105 | ;; (where {:name "user1"}))) 106 | 107 | ;; (is (= {"a" "b" "c" "d"} (get-in (select s :users_map) 108 | ;; [0 :test_map]))) 109 | ;; (truncate s :users_map)) 110 | 111 | ;; (testing "Deleting" 112 | ;; (insert s :users_map 113 | ;; {:name "user1" 114 | ;; :test_map {"a" "b" "c" "d"}}) 115 | ;; (delete s :users_map 116 | ;; (columns {:test_map "c"}) 117 | ;; (where [[= :name "user1"]])) 118 | ;; (is (= {"a" "b"} (get-in (select s :users_map) 119 | ;; [0 :test_map]))) 120 | ;; (truncate s :users_map)) 121 | ;; (drop-table s :users_map)) 122 | 123 | 124 | ;; (deftest test-set-operations 125 | ;; (create-table s :users_set 126 | ;; (column-definitions 127 | ;; {:name :varchar 128 | ;; :test_set (set-type :varchar) 129 | ;; :primary-key [:name]})) 130 | 131 | ;; (testing "Inserting" 132 | ;; (insert s :users_set 133 | ;; {:name "user1" 134 | ;; :test_set #{"str1" "str2" "str3"}}) 135 | ;; (is (= #{"str1" "str2" "str3"} (get-in (select s :users_set) 136 | ;; [0 :test_set]))) 137 | ;; (truncate s :users_set)) 138 | 139 | 140 | ;; (testing "Updating" 141 | ;; (insert s :users_set 142 | ;; {:name "user1" 143 | ;; :test_set #{}}) 144 | ;; (dotimes [i 3] 145 | ;; (dotimes [_ 2] 146 | ;; (update s :users_set 147 | ;; {:test_set [+ #{(str "str" i)}]} 148 | ;; (where {:name "user1"})))) 149 | 150 | ;; (is (= #{"str0" "str1" "str2"} (get-in (select s :users_set) 151 | ;; [0 :test_set]))) 152 | ;; (truncate s :users_set)) 153 | 154 | ;; (testing "Deleting" 155 | ;; (insert s :users_set 156 | ;; {:name "user1" 157 | ;; :test_set #{"str0" "str1" "str2"}}) 158 | ;; (update s :users_set 159 | ;; {:test_set [- #{"str0" "str1"}]} 160 | ;; (where {:name "user1"})) 161 | 162 | ;; (is (= #{"str2"} (get-in (select s :users_set) 163 | ;; [0 :test_set])))) 164 | 165 | ;; (drop-table s :users_set))) 166 | -------------------------------------------------------------------------------- /LICENSE-EPL1.0.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and 10 | b) in the case of each subsequent Contributor: 11 | i) changes to the Program, and 12 | ii) additions to the Program; 13 | where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. 14 | "Contributor" means any person or entity that distributes the Program. 15 | 16 | "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. 17 | 18 | "Program" means the Contributions distributed in accordance with this Agreement. 19 | 20 | "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 21 | 22 | 2. GRANT OF RIGHTS 23 | 24 | a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. 25 | b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. 26 | c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. 27 | d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 28 | 3. REQUIREMENTS 29 | 30 | A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: 31 | 32 | a) it complies with the terms and conditions of this Agreement; and 33 | b) its license agreement: 34 | i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; 35 | ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; 36 | iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and 37 | iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. 38 | When the Program is made available in source code form: 39 | 40 | a) it must be made available under this Agreement; and 41 | b) a copy of this Agreement must be included with each copy of the Program. 42 | Contributors may not remove or alter any copyright notices contained within the Program. 43 | 44 | Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 45 | 46 | 4. COMMERCIAL DISTRIBUTION 47 | 48 | Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. 49 | 50 | For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 51 | 52 | 5. NO WARRANTY 53 | 54 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 55 | 56 | 6. DISCLAIMER OF LIABILITY 57 | 58 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 59 | 60 | 7. GENERAL 61 | 62 | If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 63 | 64 | If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. 65 | 66 | All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. 67 | 68 | Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. 69 | 70 | This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. 71 | -------------------------------------------------------------------------------- /LICENSE-APL2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/client.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.client 16 | "Provides fundamental functions for 17 | 18 | * connecting to Cassandra nodes and clusters 19 | * configuring connections 20 | * tuning load balancing, retries, reconnection strategies and consistency settings 21 | * preparing and executing queries constructed via DSL 22 | * working with executing results" 23 | (:require [clojure.java.io :as io] 24 | [clojurewerkz.cassaforte.policies :as cp] 25 | [clojurewerkz.cassaforte.conversion :as conv]) 26 | (:import [com.datastax.driver.core Statement ResultSet ResultSetFuture Host Session Cluster 27 | Cluster$Builder SimpleStatement PreparedStatement HostDistance PoolingOptions 28 | SSLOptions JdkSSLOptions ProtocolOptions$Compression ProtocolVersion] 29 | [com.datastax.driver.dse.auth DseAuthProvider] 30 | [com.google.common.util.concurrent ListenableFuture Futures FutureCallback] 31 | [java.net URI] 32 | [javax.net.ssl TrustManagerFactory KeyManagerFactory SSLContext] 33 | [java.security KeyStore SecureRandom] 34 | [com.datastax.driver.core.exceptions DriverException])) 35 | 36 | (declare build-ssl-options select-compression) 37 | 38 | (def ^:dynamic *async* false) 39 | 40 | ;; 41 | ;; Macros 42 | ;; 43 | 44 | (defmacro async 45 | "Prepare a single statement, return prepared statement" 46 | [body] 47 | `(binding [*async* true] 48 | (do ~body))) 49 | 50 | ;; 51 | ;; Protocols 52 | ;; 53 | 54 | (defprotocol DummySession 55 | (executeAsync [_ query])) 56 | 57 | (deftype DummySessionImpl [] 58 | DummySession 59 | (executeAsync [_ query] (throw (Exception. "Not connected")))) 60 | 61 | (defprotocol BuildStatement 62 | (build-statement [query])) 63 | 64 | (extend-protocol BuildStatement 65 | String 66 | (build-statement [query] 67 | (build-statement (SimpleStatement. query))) 68 | 69 | Statement 70 | (build-statement [s] 71 | s) 72 | 73 | Object 74 | (build-statement [s] 75 | (throw (RuntimeException. (str "Can't build the statement out of the" (type s)))))) 76 | 77 | (defprotocol Listenable 78 | (add-listener [_ runnable executor])) 79 | 80 | (deftype AsyncResult [fut] 81 | clojure.lang.IDeref 82 | (deref [_] 83 | (conv/to-clj @fut)) 84 | 85 | clojure.lang.IBlockingDeref 86 | (deref [_ timeout-ms default-val] 87 | (conv/to-clj (deref fut timeout-ms default-val))) 88 | 89 | Listenable 90 | (add-listener [_ runnable executor] 91 | (.addListener fut runnable executor))) 92 | 93 | ;; 94 | ;; Fns 95 | ;; 96 | 97 | (defn ^Cluster build-cluster 98 | "Builds an instance of Cluster you can connect to. 99 | 100 | Options: 101 | * hosts: hosts to connect to 102 | * port: port, listening to incoming binary CQL connections (make sure you have `start_native_transport` set to true). 103 | * credentials: connection credentials in the form {:username username :password password} 104 | * connections-per-host: specifies core number of connections per host. 105 | * max-connections-per-host: maximum number of connections per host. 106 | * retry-policy: configures the retry policy to use for the new cluster. 107 | * load-balancing-policy: configures the load balancing policy to use for the new cluster. 108 | 109 | * consistency-level: default consistency level for all queires to be executed against this cluster 110 | * ssl: ssl options in the form {:keystore-path path :keystore-password password} Also accepts :cipher-suites with a Seq of cipher suite specs. 111 | * ssl-options: pre-built SSLOptions object (overrides :ssl) 112 | * kerberos: enables kerberos authentication" 113 | [{:keys [hosts 114 | port 115 | credentials 116 | connections-per-host 117 | max-connections-per-host 118 | consistency-level 119 | cluster-name 120 | retry-policy 121 | reconnection-policy 122 | load-balancing-policy 123 | ssl 124 | ssl-options 125 | kerberos 126 | protocol-version 127 | compression] 128 | :or {protocol-version 4}}] 129 | (let [^Cluster$Builder builder (Cluster/builder) 130 | ^PoolingOptions pooling-options (PoolingOptions.)] 131 | (when port 132 | (.withPort builder port)) 133 | (when protocol-version 134 | (.withProtocolVersion builder (ProtocolVersion/fromInt protocol-version))) 135 | (when credentials 136 | (.withCredentials builder (:username credentials) (:password credentials))) 137 | (when cluster-name 138 | (.withClusterName builder cluster-name)) 139 | (when connections-per-host 140 | (.setCoreConnectionsPerHost pooling-options HostDistance/LOCAL 141 | connections-per-host)) 142 | (when max-connections-per-host 143 | (.setMaxConnectionsPerHost pooling-options HostDistance/LOCAL 144 | max-connections-per-host)) 145 | (.withPoolingOptions builder pooling-options) 146 | (doseq [h hosts] 147 | (.addContactPoint builder h)) 148 | (when retry-policy 149 | (.withRetryPolicy builder retry-policy)) 150 | (when reconnection-policy 151 | (.withReconnectionPolicy builder reconnection-policy)) 152 | (when load-balancing-policy 153 | (.withLoadBalancingPolicy builder load-balancing-policy)) 154 | (when compression 155 | (.withCompression builder (select-compression compression))) 156 | (when ssl 157 | (.withSSL builder (build-ssl-options ssl))) 158 | (when ssl-options 159 | (.withSSL builder ssl-options)) 160 | (when kerberos 161 | (.withAuthProvider builder (DseAuthProvider.))) 162 | (.build builder))) 163 | 164 | (defn- ^JdkSSLOptions build-ssl-options 165 | [{:keys [keystore-path keystore-password cipher-suites]}] 166 | (let [keystore-stream (io/input-stream keystore-path) 167 | keystore (KeyStore/getInstance "JKS") 168 | ssl-context (SSLContext/getInstance "SSL") 169 | keymanager (KeyManagerFactory/getInstance (KeyManagerFactory/getDefaultAlgorithm)) 170 | trustmanager (TrustManagerFactory/getInstance (TrustManagerFactory/getDefaultAlgorithm)) 171 | password (char-array keystore-password) 172 | ssl-cipher-suites (if cipher-suites 173 | (into-array String cipher-suites) 174 | ;; v3.0 of the drivers dropped SSLOptions.DEFAULT_SSL_CIPHER_SUITES so we'll just re-use 175 | ;; the literal here. 176 | (into-array String ["TLS_RSA_WITH_AES_128_CBC_SHA" "TLS_RSA_WITH_AES_256_CBC_SHA"]))] 177 | (.load keystore keystore-stream password) 178 | (.init keymanager keystore password) 179 | (.init trustmanager keystore) 180 | (.init ssl-context (.getKeyManagers keymanager) (.getTrustManagers trustmanager) nil) 181 | (.. (JdkSSLOptions/builder) 182 | (withSSLContext ssl-context) 183 | (withCipherSuites ssl-cipher-suites) 184 | (build)))) 185 | 186 | (defn- ^ProtocolOptions$Compression select-compression 187 | [compression] 188 | (case compression 189 | :snappy ProtocolOptions$Compression/SNAPPY 190 | :lz4 ProtocolOptions$Compression/LZ4 191 | ProtocolOptions$Compression/NONE)) 192 | 193 | (defn- connect-or-close 194 | "Attempts to connect to the cluster or closes the cluster and reraises any errors." 195 | [^Cluster cluster & [keyspace]] 196 | (try 197 | (if keyspace 198 | (.connect cluster keyspace) 199 | (.connect cluster)) 200 | (catch DriverException e 201 | (.close cluster) 202 | (throw e)))) 203 | 204 | (defn ^Session connect 205 | "Connects to the Cassandra cluster. Use `build-cluster` to build a cluster." 206 | ([hosts] 207 | (connect-or-close (build-cluster {:hosts hosts}))) 208 | ([hosts keyspace-or-opts] 209 | (if (string? keyspace-or-opts) 210 | (connect hosts keyspace-or-opts {}) 211 | (let [keyspace (:keyspace keyspace-or-opts) 212 | opts (dissoc keyspace-or-opts :keyspace)] 213 | (if keyspace 214 | (connect hosts keyspace opts) 215 | (connect-or-close (-> opts (merge {:hosts hosts}) build-cluster)))))) 216 | ([hosts keyspace opts] 217 | (let [c (build-cluster (merge opts {:hosts hosts}))] 218 | (connect-or-close c (name keyspace))))) 219 | 220 | (defn ^Session connect-with-uri 221 | ([^String uri] 222 | (connect-with-uri uri {})) 223 | ([^String uri opts] 224 | (let [^URI u (URI. uri)] 225 | (connect [(.getHost u)] (merge {:port (.getPort u) :keyspace (-> u .getPath (.substring 1))} opts))))) 226 | 227 | (defn disconnect 228 | "1-arity version receives Session, and shuts it down. It doesn't shut down all other sessions 229 | on same cluster." 230 | [^Session session] 231 | (.close session)) 232 | 233 | (defn disconnect! 234 | "Shuts the cluster and session down. If you have other sessions, use the safe `disconnect` function instead." 235 | [^Session session] 236 | (.close (.getCluster session))) 237 | 238 | (defn shutdown-cluster 239 | "Shuts down provided cluster" 240 | [^Cluster cluster] 241 | (.close cluster)) 242 | 243 | (defn bind 244 | "Binds prepared statement to values, for example: 245 | 246 | With string statement: 247 | 248 | (let [r {:name \"Alex\" :city \"Munich\" :age (int 19)} 249 | prepared (client/prepare session 250 | (insert :users 251 | {:name ? 252 | :city ? 253 | :age ?}))] 254 | (client/execute session 255 | (client/bind prepared 256 | {:name \"Alex\" :city \"Munich\" :age (int 19)}))) 257 | " 258 | [^PreparedStatement statement values] 259 | ;; TODO: matching 260 | (if (map? values) 261 | (.bind statement (to-array (vals values))) 262 | (.bind statement (to-array values)))) 263 | 264 | (defn prepare 265 | [^Session session statement] 266 | (.prepare session statement)) 267 | 268 | ;; (defn execute-async) 269 | 270 | (defn execute 271 | "Executes a statement" 272 | ([^Session session query] 273 | (let [^Statement built-statement (build-statement query)] 274 | ;; (println built-statement) 275 | (if *async* 276 | (AsyncResult. (.executeAsync session built-statement)) 277 | (-> (.execute session built-statement) 278 | (conv/to-clj))))) 279 | ([^Session session query & {:keys [retry-policy 280 | consistency-level 281 | fetch-size 282 | enable-tracing 283 | default-timestamp]}] 284 | (let [^Statement built-statement (build-statement query)] 285 | (when default-timestamp 286 | (.setDefaultTimestamp built-statement default-timestamp)) 287 | (when enable-tracing 288 | (.enableTracing built-statement)) 289 | (when fetch-size 290 | (.setFetchSize built-statement (int fetch-size))) 291 | (when retry-policy 292 | (.setRetryPolicy built-statement retry-policy)) 293 | (when consistency-level 294 | (.setConsistencyLevel built-statement consistency-level)) 295 | (if *async* 296 | (AsyncResult. (.executeAsync session built-statement)) 297 | (-> (.execute session built-statement) 298 | (conv/to-clj)))))) 299 | 300 | (defn ^String export-schema 301 | "Exports the schema as a string" 302 | [^Session client] 303 | (-> client 304 | .getCluster 305 | .getMetadata 306 | .exportSchemaAsString)) 307 | 308 | ;; defn rebuild-schema 309 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/cql.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.cql 16 | "Main namespace for working with CQL, prepared statements. Convenience functions 17 | for key operations built on top of CQL." 18 | (:refer-clojure :exclude [update]) 19 | (:require [clojurewerkz.cassaforte.query :as query] 20 | [clojurewerkz.cassaforte.aliases :as alias] 21 | [clojurewerkz.cassaforte.client :as cc]) 22 | (:import com.datastax.driver.core.Session)) 23 | 24 | ;; 25 | ;; Imports 26 | ;; 27 | 28 | (alias/alias-ns 'clojurewerkz.cassaforte.query.query-builder) 29 | (alias/alias-ns 'clojurewerkz.cassaforte.query.dsl) 30 | (alias/alias-ns 'clojurewerkz.cassaforte.query.column) 31 | (alias/alias-ns 'clojurewerkz.cassaforte.query.types) 32 | 33 | ;; 34 | ;; Schema operations 35 | ;; 36 | 37 | (defn drop-keyspace 38 | "Drops a keyspace: results in immediate, irreversible removal of an existing keyspace, 39 | including all column families in it, and all data contained in those column families." 40 | [^Session session ks & query-params] 41 | (cc/execute session 42 | (apply query/drop-keyspace ks query-params))) 43 | 44 | (defn create-keyspace 45 | "Creates a new top-level keyspace. A keyspace is a namespace that 46 | defines a replication strategy and some options for a set of tables, 47 | similar to a database in relational databases. 48 | 49 | Example: 50 | 51 | (create-keyspace session \"new_cql_keyspace\" 52 | (with {:replication 53 | {\"class\" \"SimpleStrategy\" 54 | \"replication_factor\" 1 }})) 55 | " 56 | [^Session session keyspace & query-params] 57 | (cc/execute session 58 | (apply query/create-keyspace (cons keyspace query-params)))) 59 | 60 | (defn create-index 61 | "Creates a new (automatic) secondary index for a given (existing) 62 | column in a given table.If data already exists for the column, it will be indexed during the execution 63 | of this statement. After the index is created, new data for the column is indexed automatically at 64 | insertion time. 65 | 66 | Example, creates an index named `users_city_idx' on `users` table, `city` column: 67 | 68 | (create-index *session* :users_city_idx 69 | (on-table :users 70 | (and-column :city) 71 | (if-not-exists) 72 | " 73 | [^Session session & query-params] 74 | (cc/execute session 75 | (apply query/create-index query-params))) 76 | 77 | ;; (defn drop-index 78 | ;; "Drop an existing secondary index. The argument of the statement 79 | ;; is the index name. 80 | 81 | ;; Example, drops an index on `users` table, `city` column: 82 | 83 | ;; (drop-index th/session :users_city)" 84 | ;; [^Session session & query-params] 85 | ;; (cc/execute session 86 | ;; (apply query/drop-index query-params) )) 87 | 88 | (defn create-table 89 | "Creates a new table. A table is a set of rows (usually 90 | representing related entities) for which it defines a number of properties. 91 | 92 | A table is defined by a name, it defines the columns composing rows 93 | of the table and have a number of options. 94 | 95 | Example: 96 | 97 | (create-table *session* :userstmp 98 | (column-definitions {:name :varchar 99 | :title :varchar 100 | :primary-key [:name]})) 101 | " 102 | [^Session session & query-params] 103 | (cc/execute session 104 | (apply query/create-table query-params))) 105 | 106 | (def create-column-family create-table) 107 | 108 | (defn drop-table 109 | "Drops a table: this results in the immediate, irreversible removal of a table, including 110 | all data in it." 111 | [^Session session ks] 112 | (cc/execute session 113 | (query/drop-table ks))) 114 | 115 | (defn use-keyspace 116 | "Takes an existing keyspace name as argument and set it as the per-session current working keyspace. 117 | All subsequent keyspace-specific actions will be performed in the context of the selected keyspace, 118 | unless otherwise specified, until another USE statement is issued or the connection terminates." 119 | [^Session session ks] 120 | (cc/execute session 121 | (query/use-keyspace ks))) 122 | 123 | (defn alter-table 124 | "Alters a table definition. Use it to add new 125 | columns, drop existing ones, change the type of existing columns, or update the table options. 126 | 127 | Example: 128 | 129 | (alter-table *session* :people 130 | (rename-column :naome :name) 131 | (add-column :age :int)) 132 | " 133 | [^Session session & query-params] 134 | (cc/execute session 135 | (apply query/alter-table query-params))) 136 | 137 | (defn alter-keyspace 138 | "Alters properties of an existing keyspace. The 139 | supported properties are the same that for `create-keyspace` 140 | 141 | Example: 142 | 143 | (alter-keyspace *session* \"new_cql_keyspace\" 144 | (with {:durable-writes false 145 | :replication {\"class\" \"NetworkTopologyStrategy\" 146 | \"dc1\" 1 147 | \"dc2\" 2}})) 148 | " 149 | [^Session session & query-params] 150 | (cc/execute session 151 | (apply query/alter-keyspace query-params))) 152 | 153 | ;; 154 | ;; DB Operations 155 | ;; 156 | 157 | (defn insert 158 | "Inserts a row in a table. 159 | 160 | Note that since a row is identified by its primary key, the columns that compose it must be 161 | specified. Also, since a row only exists when it contains one value for a column not part of 162 | the primary key, one such value must be specified too. 163 | 164 | Example: 165 | 166 | (insert *session* :users 167 | {:name \"name1\" 168 | :age (int 19)}) 169 | " 170 | [^Session session & query-params] 171 | (cc/execute session 172 | (apply query/insert query-params))) 173 | 174 | 175 | (defn insert-batch 176 | "Performs a batch insert (inserts multiple records into a table at the same time). 177 | To specify additional clauses for a record (such as where or using), wrap that record 178 | and the clauses in a vector" 179 | [^Session session table records] 180 | (cc/execute session 181 | (query/batch 182 | (apply 183 | query/queries 184 | (map #(query/insert table %) records))))) 185 | 186 | 187 | (defn update 188 | "Updates one or more columns for a given row in a table. The `where` clause 189 | is used to select the row to update and must include all columns composing the PRIMARY KEY. 190 | Other columns values are specified through assignment within the `set` clause." 191 | [^Session session & query-params] 192 | (cc/execute session 193 | (apply query/update query-params))) 194 | 195 | (defn delete 196 | "Deletes columns and rows. If the `columns` clause is provided, 197 | only those columns are deleted from the row indicated by the `where` clause, please refer to 198 | doc guide (http://clojurecassandra.info/articles/kv.html) for more details. Otherwise whole rows 199 | are removed. The `where` allows to specify the key for the row(s) to delete. First argument 200 | for this function should always be table name." 201 | [^Session session table & query-params] 202 | (cc/execute session 203 | (apply query/delete (cons table query-params)))) 204 | 205 | (defn select 206 | "Retrieves one or more columns for one or more rows in a table. 207 | It returns a result set, where every row is a collection of columns returned by the query." 208 | [^Session session & query-params] 209 | (cc/execute session 210 | (apply query/select query-params))) 211 | 212 | (defn truncate 213 | "Truncates a table: permanently and irreversably removes all rows from the table, 214 | not removing the table itself." 215 | [^Session session table] 216 | (cc/execute session 217 | (query/truncate table))) 218 | 219 | ;; (defn create-user 220 | ;; [^Session session & query-params] 221 | ;; (cc/execute session 222 | ;; (apply q/create-user-query query-params))) 223 | 224 | ;; (defn alter-user 225 | ;; [^Session session & query-params] 226 | ;; (cc/execute session 227 | ;; (apply q/alter-user-query query-params))) 228 | 229 | ;; (defn drop-user 230 | ;; [^Session session & query-params] 231 | ;; (cc/execute session 232 | ;; (apply q/drop-user-query query-params))) 233 | 234 | ;; (defn grant 235 | ;; [^Session session & query-params] 236 | ;; (cc/execute session 237 | ;; (apply q/grant-query query-params))) 238 | 239 | ;; (defn revoke 240 | ;; [^Session session & query-params] 241 | ;; (cc/execute session 242 | ;; (apply q/revoke-query query-params))) 243 | 244 | ;; (defn list-users 245 | ;; [^Session session & query-params] 246 | ;; (cc/execute session 247 | ;; (apply q/list-users-query query-params))) 248 | 249 | ;; (defn list-permissions 250 | ;; [^Session session & query-params] 251 | ;; (cc/execute session 252 | ;; (apply q/list-perm-query query-params))) 253 | 254 | ;; 255 | ;; Higher level DB functions 256 | ;; 257 | 258 | (defn perform-count 259 | "Helper function to perform count on a table with given query. Count queries are slow in Cassandra, 260 | in order to get a rough idea of how many items you have in certain table, use `nodetool cfstats`, 261 | for more complex cases, you can wither do a full table scan or perform a count with this function, 262 | please note that it does not have any performance guarantees and is potentially expensive. 263 | 264 | Doesn't work as a prepared query." 265 | [^Session session table & query-params] 266 | (:count 267 | (first 268 | (apply select session table (query/count-all) query-params)))) 269 | 270 | ;; 271 | ;; Higher-level collection manipulation 272 | ;; 273 | 274 | (defn- load-chunk 275 | "Returns next chunk for the lazy table iteration" 276 | [^Session session table partition-key chunk-size last-pk] 277 | (if (nil? (first last-pk)) 278 | (select session table 279 | (query/limit chunk-size)) 280 | (select session table 281 | (query/where [[> 282 | (apply query/token partition-key) 283 | (apply query/function-call "token" last-pk)]]) 284 | (query/limit chunk-size)))) 285 | 286 | (defn iterate-table 287 | "Lazily iterates through a table, returning chunks of chunk-size." 288 | ([^Session session table partition-key chunk-size] 289 | (iterate-table session table (if (sequential? partition-key) 290 | partition-key 291 | [partition-key]) 292 | chunk-size [])) 293 | ([^Session session table partition-key chunk-size c] 294 | (lazy-cat c 295 | (let [last-pk (map #(get (last c) %) partition-key) 296 | next-chunk (load-chunk session table partition-key chunk-size last-pk)] 297 | (if (empty? next-chunk) 298 | [] 299 | (iterate-table session table partition-key chunk-size next-chunk)))))) 300 | 301 | (defn copy-table 302 | "Copies data from one table to another, transforming rows 303 | using provided function (`transform-fn`)" 304 | ([^Session session from-table to-table partition-key] 305 | (copy-table session from-table to-table partition-key 16384)) 306 | ([^Session session from-table to-table partition-key chunk-size] 307 | (copy-table session from-table to-table partition-key identity chunk-size)) 308 | ([^Session session from-table to-table partition-key transform-fn chunk-size] 309 | (doseq [row (iterate-table session from-table partition-key chunk-size)] 310 | (insert session to-table (transform-fn row))))) 311 | -------------------------------------------------------------------------------- /src/clojure/clojurewerkz/cassaforte/metadata.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2012-2014 Michael S. Klishin, Alex Petrov, and the ClojureWerkz Team 2 | ;; 3 | ;; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ;; you may not use this file except in compliance with the License. 5 | ;; You may obtain a copy of the License at 6 | ;; 7 | ;; http://www.apache.org/licenses/LICENSE-2.0 8 | ;; 9 | ;; Unless required by applicable law or agreed to in writing, software 10 | ;; distributed under the License is distributed on an "AS IS" BASIS, 11 | ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ;; See the License for the specific language governing permissions and 13 | ;; limitations under the License. 14 | 15 | (ns clojurewerkz.cassaforte.metadata 16 | "Main namespace for getting information about cluster, keyspaces, tables, etc." 17 | (:refer-clojure :exclude [update]) 18 | (:require [clojurewerkz.cassaforte.client :as cc]) 19 | (:import [com.datastax.driver.core Session KeyspaceMetadata Metadata TableMetadata 20 | ColumnMetadata TableOptionsMetadata Host UserType MaterializedViewMetadata 21 | FunctionMetadata AggregateMetadata DataType IndexMetadata ClusteringOrder 22 | AbstractTableMetadata DataType$Name] 23 | [java.util Collection Map])) 24 | 25 | 26 | ;; http://docs.datastax.com/en/drivers/java/3.3/com/datastax/driver/core/KeyspaceMetadata.html 27 | ;; http://docs.datastax.com/en/drivers/java/3.3/com/datastax/driver/core/TableMetadata.html 28 | 29 | ;; Auxiliary functions, maybe need to be re-implemented 30 | (def ^:private not-nil? (comp not nil?)) 31 | 32 | (defn- non-nil-coll [conv-func coll] 33 | (filterv not-nil? (mapv conv-func coll))) 34 | 35 | (defn- process-into-map 36 | [conv-func field coll] 37 | (into {} (non-nil-coll (fn [d] (when-let [m (conv-func d)] 38 | [(get m field) m])) 39 | coll))) 40 | 41 | (defn- into-keyed-map [^Map coll] 42 | (zipmap (map keyword (.keySet coll)) (.values coll))) 43 | 44 | ;; Functions for conversion of different objects into maps 45 | (defn- convert-data-type [^DataType dt] 46 | (when dt 47 | (let [type-args (.getTypeArguments dt) 48 | nm (.getName dt) 49 | is-udt? (= nm DataType$Name/UDT)] 50 | (-> { 51 | :name (if is-udt? 52 | (str (.getKeyspace ^UserType dt) "." (.getTypeName ^UserType dt) ) 53 | (str nm)) 54 | :collection? (.isCollection dt) 55 | :frozen? (.isFrozen dt) 56 | :user-defined? is-udt? 57 | } 58 | (cond-> (seq type-args) (assoc :type-args (mapv convert-data-type type-args))))))) 59 | 60 | (defn- convert-user-types-meta [^UserType ut-meta] 61 | (when ut-meta 62 | { 63 | :name (keyword (.getTypeName ut-meta)) 64 | :frozen? (.isFrozen ut-meta) 65 | :keyspace (keyword (.getKeyspace ut-meta)) 66 | :fields (into {} 67 | (filter not-nil? 68 | (mapv (fn [^String field-name] 69 | (when-let [ut (convert-data-type (.getFieldType ut-meta field-name))] 70 | [(keyword field-name) ut])) 71 | (.getFieldNames ut-meta)))) 72 | :cql (.asCQLQuery ut-meta) 73 | })) 74 | 75 | (defn- convert-func-meta [^FunctionMetadata fn-meta] 76 | (when fn-meta 77 | (let [args (.getArguments fn-meta)] 78 | { 79 | :cql (.asCQLQuery fn-meta) 80 | :name (.getSignature fn-meta) 81 | :simple-name (.getSimpleName fn-meta) 82 | :arguments (into {} (zipmap (map keyword (.keySet args)) 83 | (map convert-data-type (.values args)))) 84 | :language (.getLanguage fn-meta) 85 | :callable-on-nil? (.isCalledOnNullInput fn-meta) 86 | :body (.getBody fn-meta) 87 | :return-type (convert-data-type (.getReturnType fn-meta)) 88 | }))) 89 | 90 | (defn- convert-index-meta [^IndexMetadata idx-meta] 91 | (when idx-meta 92 | (let [idx-class (.getIndexClassName idx-meta)] 93 | (-> { 94 | :custom? (.isCustomIndex idx-meta) 95 | :name (keyword (.getName idx-meta)) 96 | :kind (keyword (.name (.getKind idx-meta))) 97 | :target (.getTarget idx-meta) 98 | :cql (.asCQLQuery idx-meta)} 99 | (cond-> idx-class (assoc :index-class idx-class)))))) 100 | 101 | (defn- convert-aggr-meta [^AggregateMetadata agg-meta] 102 | (when agg-meta 103 | { 104 | :name (.getSignature agg-meta) 105 | :simple-name (.getSimpleName agg-meta) 106 | :return-type (convert-data-type (.getReturnType agg-meta)) 107 | :arguments (filterv not-nil? (mapv convert-data-type (.getArgumentTypes agg-meta))) 108 | :cql (.asCQLQuery agg-meta) 109 | :state-type (convert-data-type (.getStateType agg-meta)) 110 | :state-func (.. agg-meta getStateFunc getSignature) 111 | :final-func (.. agg-meta getFinalFunc getSignature) 112 | :init-state (.getInitCond agg-meta) 113 | })) 114 | 115 | (defn- convert-column [^ColumnMetadata col-meta] 116 | (when col-meta 117 | {:static? (.isStatic col-meta) 118 | :name (keyword (.getName col-meta)) 119 | :type (convert-data-type (.getType col-meta))})) 120 | 121 | (defn- convert-table-options [^TableOptionsMetadata opts] 122 | (when opts 123 | (let [caching (.getCaching opts) 124 | compaction (.getCompaction opts) 125 | comnt (.getComment opts) 126 | compression (.getCompression opts) 127 | extensions (.getExtensions opts)] 128 | (-> { 129 | :bloom-filter-fp-chance (.getBloomFilterFalsePositiveChance opts) 130 | :crc-check-chance (.getCrcCheckChance opts) 131 | :default-ttl (.getDefaultTimeToLive opts) 132 | :compact-storage? (.isCompactStorage opts) 133 | :cdc? (.isCDC opts) 134 | :gc-grace (.getGcGraceInSeconds opts) 135 | :local-read-repair-chance (.getLocalReadRepairChance opts) 136 | :read-repair-chance (.getReadRepairChance opts) 137 | :max-index-interval (.getMaxIndexInterval opts) 138 | :min-index-interval (.getMinIndexInterval opts) 139 | :replicate-on-write? (.getReplicateOnWrite opts) 140 | :speculative-retry (.getSpeculativeRetry opts) 141 | :memtable-flush-period (.getMemtableFlushPeriodInMs opts) 142 | :populate-io-cache-on-flush? (.getPopulateIOCacheOnFlush opts) 143 | } 144 | (cond-> caching (assoc :caching (into-keyed-map caching))) 145 | (cond-> (seq comnt) (assoc :comment comnt)) 146 | (cond-> compaction (assoc :compaction (into-keyed-map compaction))) 147 | (cond-> extensions (assoc :extensions (into-keyed-map extensions))) 148 | (cond-> compression (assoc :compression (into-keyed-map compression))))))) 149 | 150 | ;; TODO: Decide - do we need to include primary key, partition key, clustering & regular 151 | ;; columns as separate slots? Or it's better to leave filtering to user? 152 | (defn- convert-abstract-table-meta [^AbstractTableMetadata at-meta] 153 | (when at-meta 154 | (let [id (.getId at-meta) 155 | partition-key-names (non-nil-coll (fn [^ColumnMetadata v] (keyword (.getName v))) 156 | (.getPartitionKey at-meta)) 157 | clustering-names (non-nil-coll (fn [^ColumnMetadata v] (keyword (.getName v))) 158 | (.getClusteringColumns at-meta)) 159 | non-regular (into {} (concat (mapv #(vector % :partition-key) partition-key-names) 160 | (mapv #(vector % :clustering) clustering-names))) 161 | columns (non-nil-coll convert-column (.getColumns at-meta)) 162 | columns (mapv #(assoc % :kind (get non-regular (:name %) :regular)) columns) 163 | primary-key (filterv #(not= (:kind %) :regular) columns) 164 | partition-key (filterv #(= (:kind %) :partition-key) columns) 165 | clustering-columns (filterv #(= (:kind %) :clustering) columns) 166 | clustering-order (.getClusteringOrder at-meta) 167 | options (convert-table-options (.getOptions at-meta)) 168 | regular-columns (filterv #(= (:kind %) :regular) columns)] 169 | (-> { 170 | :name (keyword (.getName at-meta)) 171 | :cql (.asCQLQuery at-meta) 172 | :columns columns 173 | :primary-key primary-key 174 | :partition-key partition-key 175 | } 176 | (cond-> (seq regular-columns) (assoc :regular-columns regular-columns)) 177 | (cond-> (seq clustering-columns) 178 | (assoc :clustering-columns 179 | (mapv (fn [v1 ^ClusteringOrder v2] 180 | (assoc v1 :order (keyword (.name v2)))) 181 | clustering-columns clustering-order))) 182 | (cond-> id (assoc :id id)) 183 | (cond-> options (assoc :options options)))))) 184 | 185 | (defn- convert-mv-meta [^MaterializedViewMetadata mv-meta] 186 | (when mv-meta 187 | (assoc (convert-abstract-table-meta mv-meta) 188 | :base-table (keyword (.. mv-meta getBaseTable getName))))) 189 | 190 | (defn- convert-table-meta [^TableMetadata table-meta] 191 | (when table-meta 192 | (let [indexes (process-into-map convert-index-meta :name (.getIndexes table-meta)) 193 | mvs (process-into-map convert-mv-meta :name (.getViews table-meta))] 194 | (-> (convert-abstract-table-meta table-meta) 195 | (cond-> (seq indexes) (assoc :indexes indexes)) 196 | (cond-> (seq mvs) (assoc :materialized-views mvs)))))) 197 | 198 | (defn- convert-keyspace-meta 199 | "Converts KeyspaceMetadata into Clojure map" 200 | [^KeyspaceMetadata ks-meta detailed?] 201 | (when ks-meta 202 | (let [replication (.getReplication ks-meta) 203 | tbl-meta (.getTables ks-meta) 204 | tables (if detailed? 205 | (process-into-map convert-table-meta :name tbl-meta) 206 | (mapv (fn [^TableMetadata v] 207 | (keyword (.getName v))) (seq tbl-meta))) 208 | ut-meta (.getUserTypes ks-meta) 209 | user-types (if detailed? 210 | (process-into-map convert-user-types-meta :name ut-meta) 211 | (mapv (fn [^UserType v] 212 | (keyword (.getTypeName v))) (seq ut-meta))) 213 | fn-meta (.getFunctions ks-meta) 214 | functions (if detailed? 215 | (process-into-map convert-func-meta :name fn-meta) 216 | (mapv (fn [^FunctionMetadata v] 217 | (.getSignature v)) (seq fn-meta))) 218 | aggr-meta (.getAggregates ks-meta) 219 | aggregates (if detailed? 220 | (process-into-map convert-aggr-meta :name aggr-meta) 221 | (mapv (fn [^AggregateMetadata v] 222 | (.getSignature v)) (seq aggr-meta))) 223 | mvs-meta (.getMaterializedViews ks-meta) 224 | materialized-views (if detailed? 225 | (process-into-map convert-mv-meta :name mvs-meta) 226 | (mapv (fn [^MaterializedViewMetadata v] 227 | (keyword (.getName v))) (seq mvs-meta)))] 228 | (-> { 229 | :replication (into-keyed-map replication) 230 | :name (keyword (.getName ks-meta)) 231 | :durable-writes (.isDurableWrites ks-meta) 232 | } 233 | (cond-> (seq tables) (assoc :tables tables)) 234 | (cond-> (seq user-types) (assoc :user-types user-types)) 235 | (cond-> (seq functions) (assoc :functions functions)) 236 | (cond-> (seq aggregates) (assoc :aggregates aggregates)) 237 | (cond-> (seq materialized-views) (assoc :materialized-views materialized-views)) 238 | (cond-> detailed? (assoc :cql (.asCQLQuery ks-meta))))))) 239 | 240 | (defn- ^Metadata get-cluster-meta [^Session session] 241 | (when session 242 | (when-let [cluster (.getCluster session)] 243 | (.getMetadata cluster)))) 244 | 245 | (defn- ^KeyspaceMetadata get-keyspace-meta [^Session session ks] 246 | (when-let [cluster-meta ^Metadata (get-cluster-meta session)] 247 | (.getKeyspace cluster-meta (name ks)))) 248 | 249 | (defn keyspace 250 | "Describes a keyspace. 251 | Verbosity is regulated by :detailed? parameter that is equal to false by default." 252 | [^Session session ks & {:keys [detailed?] :or {detailed? false}}] 253 | (convert-keyspace-meta (get-keyspace-meta session ks) detailed?)) 254 | 255 | (defn keyspaces 256 | "Describes all available keyspaces. 257 | Verbosity is regulated by :detailed? parameter that is equal to false by default." 258 | [^Session session & {:keys [detailed?] :or {detailed? false}}] 259 | (when-let [cluster-meta ^Metadata (get-cluster-meta session)] 260 | (process-into-map #(convert-keyspace-meta % detailed?) 261 | :name (.getKeyspaces cluster-meta)))) 262 | 263 | (defn table 264 | "Describes a table" 265 | [^Session session ks table] 266 | (when-let [ks-meta (get-keyspace-meta session ks)] 267 | (convert-table-meta (.getTable ks-meta (name table))))) 268 | 269 | (defn tables 270 | "Returns descriptions of all the tables" 271 | [^Session session ks] 272 | (when-let [ks-meta (get-keyspace-meta session ks)] 273 | (process-into-map convert-table-meta :name (.getTables ks-meta)))) 274 | 275 | (defn index 276 | "Describes an index" 277 | [^Session session ks table index] 278 | (when-let [ks-meta (get-keyspace-meta session ks)] 279 | (when-let [tbl (.getTable ks-meta (name table))] 280 | (convert-index-meta (.getIndex tbl (name index)))))) 281 | 282 | (defn indexes 283 | "Returns descriptions of indices" 284 | [^Session session ks table] 285 | (when-let [ks-meta (get-keyspace-meta session ks)] 286 | (when-let [tbl (.getTable ks-meta (name table))] 287 | (process-into-map convert-index-meta :name (.getIndexes tbl))))) 288 | 289 | (defn materialized-view 290 | "Describes a materialized view" 291 | [^Session session ks mv] 292 | (when-let [ks-meta (get-keyspace-meta session ks)] 293 | (convert-mv-meta (.getMaterializedView ks-meta (name mv))))) 294 | 295 | (defn materialized-views 296 | "Returns descriptions of all materialized views" 297 | [^Session session ks] 298 | (when-let [ks-meta (get-keyspace-meta session ks)] 299 | (process-into-map convert-mv-meta :name (.getMaterializedViews ks-meta)))) 300 | 301 | (defn user-type 302 | "Describes a " 303 | [^Session session ks utype] 304 | (when-let [ks-meta (get-keyspace-meta session ks)] 305 | (convert-user-types-meta (.getUserType ks-meta (name utype))))) 306 | 307 | (defn user-types 308 | "Returns descriptions of all user types" 309 | [^Session session ks] 310 | (when-let [ks-meta (get-keyspace-meta session ks)] 311 | (process-into-map convert-user-types-meta :name (.getUserTypes ks-meta)))) 312 | 313 | (defn aggregate 314 | "Describes a " 315 | [^Session session ks aggr ^Collection arg-types] 316 | (when-let [ks-meta (get-keyspace-meta session ks)] 317 | (convert-aggr-meta (.getAggregate ks-meta (name aggr) arg-types)))) 318 | 319 | (defn aggregates 320 | "Returns descriptions of all aggregates" 321 | [^Session session ks] 322 | (when-let [ks-meta (get-keyspace-meta session ks)] 323 | (process-into-map convert-aggr-meta :name (.getAggregates ks-meta)))) 324 | 325 | (defn function 326 | "Describes a " 327 | [^Session session ks func ^Collection arg-types] 328 | (when-let [ks-meta (get-keyspace-meta session ks)] 329 | (convert-func-meta (.getFunction ks-meta (name func) arg-types)))) 330 | 331 | (defn functions 332 | "Returns descriptions of all functions" 333 | [^Session session ks] 334 | (when-let [ks-meta (get-keyspace-meta session ks)] 335 | (process-into-map convert-func-meta :name (.getFunctions ks-meta)))) 336 | 337 | (defn columns 338 | "Describes columns of a table" 339 | [^Session session ks tbl-name] 340 | (:columns (table session ks tbl-name))) 341 | 342 | (defn- get-host-info [^Host host detailed?] 343 | (when host 344 | (-> {:rack (.getRack host) 345 | :datacenter (.getDatacenter host) 346 | :up? (.isUp host) 347 | :state (.getState host) 348 | :cassandra-version (str (.getCassandraVersion host)) 349 | ;; :dse-version (str (.getDseVersion host)) 350 | ;; :dse-workloads (.getDseWorkloads host) 351 | :socket-address (str (.getSocketAddress host)) 352 | :listen-address (str (.getListenAddress host)) 353 | :address (str (.getAddress host)) 354 | :broadcast-address (str (.getBroadcastAddress host)) 355 | } 356 | (cond-> detailed? (assoc :tokens (.getTokens host)))))) 357 | 358 | (defn- get-hosts-impl [^Metadata cluster-meta detailed?] 359 | (when-let [hosts (.getAllHosts cluster-meta)] 360 | (non-nil-coll #(get-host-info % detailed?) hosts))) 361 | 362 | (defn hosts 363 | "Returns information about hosts in cluster associated with session. 364 | Verbosity is regulated by :detailed? parameter that is equal to false by default." 365 | [^Session session & {:keys [detailed?] :or {detailed? false}}] 366 | (when-let [cluster-meta ^Metadata (get-cluster-meta session)] 367 | (get-hosts-impl cluster-meta detailed?))) 368 | 369 | (defn cluster 370 | "Describes cluster associated with session. 371 | Verbosity is regulated by :detailed? parameter that is equal to false by default." 372 | [^Session session & {:keys [detailed?] :or {detailed? false}}] 373 | (when-let [cluster-meta ^Metadata (get-cluster-meta session)] 374 | (-> { 375 | :name (keyword (.getClusterName cluster-meta)) 376 | :hosts (get-hosts-impl cluster-meta detailed?) 377 | :partitioner (.getPartitioner cluster-meta) 378 | :keyspaces (process-into-map #(convert-keyspace-meta % detailed?) 379 | :name (.getKeyspaces cluster-meta)) 380 | :schema-agreed? (.checkSchemaAgreement cluster-meta) 381 | } 382 | ;; TODO: do we need it? 383 | (cond-> detailed? (assoc :schema (.exportSchemaAsString cluster-meta)))))) 384 | 385 | (defn cluster-schema 386 | "Returns full schema for cluster associated with session." 387 | [^Session session] 388 | (when-let [cluster-meta ^Metadata (get-cluster-meta session)] 389 | (.exportSchemaAsString cluster-meta))) 390 | 391 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/cql_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.cql-test 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojurewerkz.cassaforte.test-helper :refer [*session* with-table with-temporary-keyspace]] 4 | [clojurewerkz.cassaforte.client :as client] 5 | [clojurewerkz.cassaforte.policies :as cp] 6 | [clojurewerkz.cassaforte.uuids :as uuids] 7 | 8 | [clj-time.format :as tf] 9 | [clj-time.coerce :as cc] 10 | [clj-time.core :refer [seconds ago before? date-time] :as tc] 11 | 12 | [clojurewerkz.cassaforte.cql :refer :all] 13 | [clojurewerkz.cassaforte.query :as query] 14 | [clojure.test :refer :all])) 15 | 16 | (use-fixtures :each with-temporary-keyspace) 17 | 18 | (deftest test-insert 19 | (let [r {:name "Alex" :city "Munich" :age (int 19)}] 20 | (insert *session* :users r) 21 | (is (= r (first (select *session* :users)))))) 22 | 23 | (deftest test-insert-async 24 | (let [r {:name "Alex" :city "Munich" :age (int 19)}] 25 | (is (= [] 26 | (-> (insert *session* :users r) 27 | client/async 28 | deref))) 29 | (is (= r 30 | (-> (select *session* :users) 31 | client/async 32 | deref 33 | first))))) 34 | 35 | (deftest test-update 36 | (testing "Simple updates" 37 | (let [r {:name "Alex" 38 | :city "Munich" 39 | :age (int 19)}] 40 | (insert *session* :users r) 41 | (is (= r (first (select *session* :users)))) 42 | (update *session* :users 43 | {:age (int 25)} 44 | (where {:name "Alex"})) 45 | (is (= {:name "Alex" :city "Munich" :age (int 25)} 46 | (first (select *session* :users)))))) 47 | 48 | (testing "One of many update" 49 | (dotimes [i 3] 50 | (insert *session* :user_posts {:username "user1" 51 | :post_id (str "post" i) 52 | :body (str "body" i)})) 53 | 54 | (update *session* :user_posts 55 | {:body "bodynew"} 56 | (where {:username "user1" 57 | :post_id "post1"})) 58 | (is (= "bodynew" 59 | (get-in 60 | (select *session* :user_posts 61 | (where {:username "user1" 62 | :post_id "post1"})) 63 | [0 :body]))))) 64 | 65 | (deftest test-update-with-compound-key 66 | (let [t :events_by_device_id_and_date 67 | fmt (tf/formatter "yyyy-MM-dd") 68 | id "device-000000001" 69 | qc [[= :device_id id] 70 | [= :date "2014-11-13"] 71 | [= :created_at (uuids/start-of (cc/to-long (date-time 2014 11 13 12)))]]] 72 | (testing "Bulk update" 73 | (truncate *session* t) 74 | (is (= 0 (perform-count *session* t))) 75 | (doseq [i (range 1 15)] 76 | (let [dt (date-time 2014 11 i 12) 77 | td (uuids/start-of (cc/to-long dt))] 78 | 79 | (insert *session* t {:created_at td 80 | :device_id id 81 | :date (tf/unparse fmt dt) 82 | :payload (str "body" i)}))) 83 | (is (= 14 (perform-count *session* t))) 84 | (is (= 14 (count (select *session* t)))) 85 | (is (= 1 (count (select *session* t (where qc))))) 86 | (update *session* t 87 | {:payload "updated payload"} 88 | (where qc)) 89 | (let [x (first (select *session* t (where qc)))] 90 | (is (= "updated payload" (:payload x))))))) 91 | 92 | (deftest test-delete-row 93 | (dotimes [i 3] 94 | (insert *session* :users {:name (str "name" i) 95 | :age (int i)})) 96 | (is (= 3 (perform-count *session* :users))) 97 | (delete *session* :users 98 | (where {:name "name1"})) 99 | (is (= 2 (perform-count *session* :users)))) 100 | 101 | (deftest test-delete-column 102 | (insert *session* :users {:name "name1" :age (int 19)}) 103 | (delete *session* :users 104 | (columns :age) 105 | (where {:name "name1"})) 106 | (is (nil? (:age (select *session* :users))))) 107 | 108 | (deftest test-insert-with-timestamp 109 | (let [r {:name "Alex" :city "Munich" :age (int 19)}] 110 | (insert *session* :users 111 | r 112 | (using :timestamp (.getTime (java.util.Date.)))) 113 | (is (= r (first (select *session* :users)))))) 114 | 115 | (deftest test-ttl 116 | (dotimes [i 3] 117 | (insert *session* :users {:name (str "name" i) 118 | :city (str "city" i) 119 | :age (int i)} 120 | (using :ttl (int 2)))) 121 | (is (= 3 (perform-count *session* :users))) 122 | (Thread/sleep 2100) 123 | (is (= 0 (perform-count *session* :users)))) 124 | 125 | 126 | (deftest test-counter 127 | (testing "Increment by" 128 | (update *session* :user_counters 129 | {:user_count (increment-by 5)} 130 | (where {:name "user1"})) 131 | 132 | (is (= 5 (-> (select *session* :user_counters) 133 | first 134 | :user_count)))) 135 | 136 | (testing "Decrement by" 137 | (update *session* :user_counters 138 | {:user_count (decrement-by 5)} 139 | (where {:name "user1"})) 140 | 141 | (is (= 0 (-> (select *session* :user_counters) 142 | first 143 | :user_count)))) 144 | 145 | (testing "Increment with explicit params" 146 | (update *session* :user_counters 147 | {:user_count (increment-by 500)} 148 | (where {:name "user1"})) 149 | (is (= 500 (-> (select *session* :user_counters) 150 | first 151 | :user_count)))) 152 | 153 | (testing "Decrement with explicit params" 154 | (update *session* :user_counters 155 | {:user_count (increment-by 5000000)} 156 | (where {:name "user1"})) 157 | (is (= 5000500 (-> (select *session* :user_counters) 158 | first 159 | :user_count)))) 160 | 161 | (testing "Increment with a large number" 162 | (update *session* :user_counters 163 | {:user_count (increment-by 50000000000000)} 164 | (where {:name "user1"})) 165 | (is (= 50000005000500 (-> *session* 166 | (select :user_counters) 167 | first 168 | :user_count))))) 169 | 170 | (deftest test-counter-2 171 | (dotimes [i 100] 172 | (update *session* :user_counters 173 | {:user_count (increment)} 174 | (where {:name "user1"}))) 175 | (is (= 100 (-> (select *session* :user_counters) 176 | first 177 | :user_count)))) 178 | 179 | (deftest test-index-filtering-range 180 | (create-index *session* :city 181 | (on-table :users) 182 | (and-column :city)) 183 | (create-index *session* :age 184 | (on-table :users) 185 | (and-column :age)) 186 | 187 | (dotimes [i 10] 188 | (insert *session* :users {:name (str "name_" i) 189 | :city "Munich" 190 | :age (int i)})) 191 | 192 | (is (= (set (range 6 10)) 193 | (->> (select *session* :users 194 | (where [[= :city "Munich"] 195 | [> :age (int 5)]]) 196 | (allow-filtering)) 197 | (map :age) 198 | set)))) 199 | 200 | (deftest test-index-filtering-range-alt-syntax 201 | (create-index *session* :city 202 | (on-table :users) 203 | (and-column :city)) 204 | (create-index *session* :age 205 | (on-table :users) 206 | (and-column :age)) 207 | 208 | (dotimes [i 10] 209 | (insert *session* :users {:name (str "name_" i) 210 | :city "Munich" 211 | :age (int i)})) 212 | 213 | (let [res (select *session* :users 214 | (where [[= :city "Munich"] 215 | [> :age (int 5)]]) 216 | (allow-filtering))] 217 | (is (= (set (range 6 10)) 218 | (->> res 219 | (map :age) 220 | set)))) 221 | (truncate *session* :users)) 222 | 223 | (deftest test-index-exact-match 224 | (create-index *session* :city 225 | (on-table :users) 226 | (and-column :city)) 227 | (create-index *session* :age 228 | (on-table :users) 229 | (and-column :age)) 230 | 231 | (dotimes [i 10] 232 | (insert *session* :users {:name (str "name_" i) 233 | :city (str "city_" i) 234 | :age (int i)})) 235 | 236 | (let [res (select *session* :users 237 | (where {:city "city_5"}) 238 | (allow-filtering))] 239 | (is (= 5 240 | (-> res 241 | first 242 | :age)))) 243 | (truncate *session* :users)) 244 | 245 | (deftest test-select-where 246 | (insert *session* :users {:name "Alex" 247 | :city "Munich" 248 | :age (int 19)}) 249 | (insert *session* :users {:name "Robert" 250 | :city "Berlin" 251 | :age (int 25)}) 252 | (insert *session* :users {:name "Sam" 253 | :city "San Francisco" 254 | :age (int 21)}) 255 | 256 | (is (= "Munich" (get-in (select *session* :users (where {:name "Alex"})) [0 :city])))) 257 | 258 | (deftest test-select-where-without-fetch 259 | (insert *session* :users {:name "Alex" 260 | :city "Munich" 261 | :age (int 19)}) 262 | (insert *session* :users {:name "Robert" 263 | :city "Berlin" 264 | :age (int 25)}) 265 | (insert *session* :users {:name "Sam" 266 | :city "San Francisco" 267 | :age (int 21)}) 268 | 269 | (is (= "Munich" (get-in (client/execute *session* 270 | "SELECT * FROM users where name='Alex';" 271 | :fetch-size Integer/MAX_VALUE) [0 :city])))) 272 | 273 | (deftest test-select-in 274 | (insert *session* :users {:name "Alex" 275 | :city "Munich" 276 | :age (int 19)}) 277 | (insert *session* :users {:name "Robert" 278 | :city "Berlin" 279 | :age (int 25)}) 280 | (insert *session* :users {:name "Sam" 281 | :city "San Francisco" 282 | :age (int 21)}) 283 | 284 | (let [users (select *session* :users 285 | (where [[:in :name ["Alex" "Robert"]]]))] 286 | (is (= "Munich" (get-in users [0 :city]))) 287 | (is (= "Berlin" (get-in users [1 :city]))))) 288 | 289 | (deftest test-select-order-by 290 | (dotimes [i 3] 291 | (insert *session* :user_posts 292 | {:username "Alex" 293 | :post_id (str "post" i) 294 | :body (str "body" i)})) 295 | 296 | (is (= [{:post_id "post0"} 297 | {:post_id "post1"} 298 | {:post_id "post2"}] 299 | (select *session* :user_posts 300 | (columns :post_id) 301 | (where {:username "Alex"}) 302 | (order-by :post_id)))) 303 | 304 | (is (= [{:post_id "post2"} 305 | {:post_id "post1"} 306 | {:post_id "post0"}] 307 | (select *session* :user_posts 308 | (columns :post_id) 309 | (where {:username "Alex"}) 310 | (order-by (desc :post_id)))))) 311 | 312 | (deftest test-timeuuid-now-and-unix-timestamp-of 313 | (let [dt (-> 2 seconds ago) 314 | ts (cc/to-long dt)] 315 | (dotimes [i 20] 316 | (insert *session* :events 317 | {:created_at (now) 318 | :message (format "Message %d" i)})) 319 | (let [xs (select *session* :events 320 | (unix-timestamp-of :created_at) 321 | (limit 5)) 322 | ts' (or 323 | (get (first xs) (keyword "unixTimestampOf(created_at)")) 324 | (get (first xs) (keyword "system.unixtimestampof(created_at)")))] 325 | (is (> ts' ts))))) 326 | 327 | (deftest test-timeuuid-dateof 328 | (let [dt (-> 2 seconds ago)] 329 | (dotimes [i 20] 330 | (insert *session* :events 331 | {:created_at (now) 332 | :message (format "Message %d" i)})) 333 | (let [xs (select *session* :events 334 | (date-of :created_at) 335 | (limit 5)) 336 | dt' (cc/from-date (get (first xs) (keyword "dateOf(created_at)")))] 337 | (is (before? dt dt'))))) 338 | 339 | (deftest test-timeuuid-min-open-range-query 340 | (dotimes [i 10] 341 | (let [dt (date-time 2014 11 17 23 i)] 342 | (insert *session* :events_for_in_and_range_query 343 | {:created_at (uuids/start-of (cc/to-long dt)) 344 | :city "London, UK" 345 | :message (format "Message %d" i)}))) 346 | (let [xs (select *session* :events_for_in_and_range_query 347 | (where [[:in :message ["Message 7" "Message 8" "Message 9" "Message 10"]] 348 | [>= :created_at (-> (date-time 2014 11 17 23 8) 349 | .toDate 350 | min-timeuuid)]]))] 351 | (is (= #{"Message 8" "Message 9"} 352 | (set (map :message xs)))))) 353 | 354 | (deftest test-timeuuid-min-max-timeuuid-range-query 355 | (dotimes [i 10] 356 | (let [dt (date-time 2014 11 17 23 i)] 357 | (insert *session* :events_for_in_and_range_query 358 | {:created_at (uuids/start-of (cc/to-long dt)) 359 | :message (format "Message %d" i)}))) 360 | (let [xs (select *session* :events_for_in_and_range_query 361 | (where [[:in :message ["Message 5" "Message 6" "Message 7" 362 | "Message 8" "Message 9"]] 363 | [>= :created_at (-> (date-time 2014 11 17 23 6) 364 | .toDate 365 | min-timeuuid)] 366 | [<= :created_at (-> (date-time 2014 11 17 23 8) 367 | .toDate 368 | max-timeuuid)]]))] 369 | (is (= #{"Message 6" "Message 7" "Message 8"} 370 | (set (map :message xs)))))) 371 | 372 | 373 | (deftest test-select-range-query 374 | (create-table *session* :tv_series 375 | (column-definitions {:series_title :varchar 376 | :episode_id :int 377 | :episode_title :text 378 | :primary-key [:series_title :episode_id]})) 379 | (dotimes [i 20] 380 | (insert *session* :tv_series 381 | {:series_title "Futurama" 382 | :episode_id i 383 | :episode_title (str "Futurama Title " i)}) 384 | (insert *session* :tv_series 385 | {:series_title "Simpsons" 386 | :episode_id i 387 | :episode_title (str "Simpsons Title " i)})) 388 | 389 | (is (= (set (range 11 20)) 390 | (->> (select *session* :tv_series 391 | (where [[= :series_title "Futurama"] 392 | [> :episode_id 10]])) 393 | (map :episode_id ) 394 | set))) 395 | 396 | (is (= (set (range 11 16)) 397 | (->> (select *session* :tv_series 398 | (where [[= :series_title "Futurama"] 399 | [> :episode_id 10] 400 | [<= :episode_id 15]])) 401 | (map :episode_id) 402 | set))) 403 | 404 | (is (= (set (range 0 15)) 405 | (->> (select *session* :tv_series 406 | (where [[= :series_title "Futurama"] 407 | [< :episode_id 15]])) 408 | (map :episode_id) 409 | set))) 410 | 411 | (drop-table *session* :tv_series)) 412 | 413 | (deftest test-paginate 414 | (with-table :tv_series 415 | (dotimes [i 20] 416 | (insert *session* :tv_series 417 | {:series_title "Futurama" 418 | :episode_id i 419 | :episode_title (str "Futurama Title " i)}) 420 | (insert *session* :tv_series 421 | {:series_title "Simpsons" 422 | :episode_id i 423 | :episode_title (str "Simpsons Title " i)})) 424 | 425 | 426 | (is (= (set (range 0 10)) 427 | (->> (select *session* :tv_series 428 | (paginate :key :episode_id :per-page 10 429 | :where {:series_title "Futurama"})) 430 | (map :episode_id) 431 | set))) 432 | 433 | (is (= (set (range 11 20)) 434 | (->> (select *session* :tv_series 435 | (paginate :key :episode_id :per-page 10 :last-key 10 436 | :where {:series_title "Futurama"})) 437 | (map :episode_id) 438 | set))))) 439 | 440 | (deftest test-insert-with-consistency-level 441 | (let [r {:name "Alex" :city "Munich" :age (int 19)}] 442 | (client/execute *session* 443 | "INSERT INTO users (name, city, age) VALUES ('Alex', 'Munich', 19);" 444 | :consistency-level (cp/consistency-level :quorum)) 445 | (is (= r (first (select *session* :users (limit 1))))))) 446 | 447 | (deftest test-insert-with-forced-prepared-statements 448 | (let [r {:name "Alex" :city "Munich" :age (int 19)} 449 | prepared (client/prepare *session* 450 | (query/insert :users 451 | {:name ? 452 | :city ? 453 | :age ?}))] 454 | (client/execute *session* 455 | (client/bind prepared 456 | {:name "Alex" :city "Munich" :age (int 19)})) 457 | (is (= r (first (select *session* :users (limit 1))))))) 458 | 459 | (deftest test-raw-cql-insert 460 | (client/execute *session* "INSERT INTO users (name, city, age) VALUES ('Alex', 'Munich', 19);") 461 | (is (= {:name "Alex" :city "Munich" :age (int 19)} 462 | (first (client/execute *session* "SELECT * FROM users;")))) 463 | (client/execute *session* "TRUNCATE users;")) 464 | 465 | (deftest test-raw-cql-with-prepared 466 | (client/execute *session* 467 | (client/bind 468 | (client/prepare *session* "INSERT INTO users (name, city, age) VALUES (?, ?, ?);") 469 | ["Alex" "Munich" (int 19)])) 470 | (is (= {:name "Alex" :city "Munich" :age (int 19)} 471 | (first (client/execute *session* "SELECT * FROM users;"))))) 472 | 473 | (deftest test-insert-nils 474 | (let [r {:name "Alex" :city "Munich" :age nil}] 475 | (client/execute *session* 476 | (client/bind 477 | (client/prepare *session* "INSERT INTO users (name, city, age) VALUES (?, ?, ?);") 478 | ["Alex" "Munich" nil])) 479 | (is (= r (first (select *session* :users)))))) 480 | 481 | (deftest test-insert-batch-plain-without-prepared-statements 482 | (let [input [{:name "Alex" :city "Munich" :age (int 19)} 483 | {:name "Robert" :city "Berlin" :age (int 25)}]] 484 | (insert-batch *session* :users input) 485 | (is (= (sort-by :name input) 486 | (sort-by :name (select *session* :users)))))) 487 | 488 | (deftest test-insert-with-atomic-batch-without-prepared-statements 489 | (client/execute *session* 490 | (query/unlogged-batch 491 | (query/queries 492 | (query/insert :users 493 | {:name "Alex" :city "Munich" :age (int 19)}) 494 | (query/insert :users 495 | {:name "Fritz" :city "Hamburg" :age (int 28)})))) 496 | (is (= [{:name "Alex" :city "Munich" :age (int 19)} 497 | {:name "Fritz" :city "Hamburg" :age (int 28)}] 498 | (sort-by :name (select *session* :users))))) 499 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## Changes between 2.1.0 and 3.0.0 2 | 3 | ### Simplified Keyspace Inclusion 4 | 5 | You can now include `clojurewerkz.cassaforte.cql` keyspace to execute 6 | all the queries as statements without having to include the `query` 7 | namespace explicitly 8 | 9 | ### Major performance improvements 10 | 11 | Cassaforte has undergone a big change of moving out from `hayt` to use 12 | `QueryBuilder` provided by the DataStax `java-driver`. Now many queries 13 | got much faster because of the way QueryBuilder is creating statements: 14 | they're not getting all inlined/serialized into Strings and embedded 15 | into the Query anymore but are transferred in a binary form. 16 | 17 | Conversion to Clojure data types was previously causing high GC pressure 18 | and lots of Reflection lookups, as it was using Clojure Protocols. 19 | Right now we're using more lightweight constructs which reduce GC 20 | pressure and amount of lookups, replacing them by the direct calls. 21 | 22 | ### Using is now accepting a `hash-map` 23 | 24 | Right now, you have to wrap statements withing using within `{}`, for 25 | example: 26 | 27 | ```clj 28 | (insert *session* :users {:name "Alex"} 29 | (using {:ttl (int 2)})) 30 | ``` 31 | 32 | ### Aggregate/Column Specifiers aren't nested within columns 33 | 34 | You can now perform `count(*)` queries as follows 35 | 36 | ```clj 37 | (select :foo 38 | (count-all)) 39 | ``` 40 | 41 | In order to use `fcall`, you can specify it in the same way you'd usually 42 | specify columns: 43 | 44 | ```clj 45 | (select :foo 46 | (fcall "intToBlob" (cname "b"))) 47 | ``` 48 | 49 | To perform `unixTimestampOf` conversion, you can use `unix-timestamp-of` 50 | 51 | ```clj 52 | (select :events 53 | (unix-timestamp-of :created_at)) 54 | ``` 55 | 56 | To make an explicit `*` query, you can use `(all)`, however if no columns are 57 | specified, it will be added implicitly: 58 | 59 | ```clj 60 | (select (all) 61 | (from :foo :bar)) 62 | ``` 63 | 64 | You can also specify columns separately now: 65 | 66 | ```clj 67 | (select "table-name" 68 | (column "first") 69 | (column "second")) 70 | ``` 71 | 72 | All the mentioned operations can be also used in combination. 73 | 74 | ### `allow-filtering` doesn't require any arguments now 75 | 76 | You can just use `allow-filtering` without passing `true` to it: 77 | 78 | ```clj 79 | (select :foo 80 | (where [[= :foo "bar"]]) 81 | (order-by (asc :foo)) 82 | (allow-filtering)) 83 | ``` 84 | 85 | Previous syntax works where you could pass `true` just as well. 86 | 87 | ### Create Index operations are now much more explicit. 88 | 89 | New API is much more intuitive and explicit: you can specify if you'd like to create 90 | an index on `column` or `keys`: 91 | 92 | ```clj 93 | (create-index "foo" 94 | (on-table "bar") 95 | (and-column "baz")) 96 | 97 | (create-index "foo" 98 | (on-table "bar") 99 | (and-keys-of-column "baz")) 100 | ``` 101 | 102 | You can find more inofrmation on creating indexes on `keys` (here)[http://docs.datastax.com/en/cql/3.1/cql/cql_reference/create_index_r.html?scroll=reference_ds_eqm_nmd_xj__CreatIdxCollKey]. 103 | 104 | ### Increments/decrements API changed 105 | 106 | Now, in order to counters, you can use `increment`, `increment-by`, `decrement` and `decrement-by`: 107 | 108 | ```clj 109 | (update :foo 110 | {:a (increment-by 1)}) 111 | 112 | (update :foo 113 | {:a (increment)}) 114 | 115 | (update :foo 116 | {:a (decrement-by 1)}) 117 | 118 | (update :foo 119 | {:a (decrement)}) 120 | ``` 121 | 122 | ## Changes between 2.0.0 and 2.1.0 123 | 124 | ### Fetch Size 125 | 126 | In order to specify the fetch size, you can use `execute`: 127 | 128 | ```clojure 129 | (client/execute s 130 | "SELECT * FROM users where name='Alex';" 131 | :fetch-size Integer/MAX_VALUE) 132 | ``` 133 | 134 | Same can be done with prepared statements: 135 | 136 | ```clojure 137 | (let [prepared (client/prepare (insert s :users 138 | {:name ? 139 | :city ? 140 | :age ?})) 141 | r {:name "Alex" :city "Munich" :age (int 19)}] 142 | (client/execute s 143 | (client/bind prepared ["Alex" "Munich" (int 19)])) 144 | :fetch-size Integer/MAX_VALUE) 145 | ``` 146 | 147 | ### Prepared statements 148 | 149 | It is now possible to prepare statements for later execution, for example: 150 | 151 | ```clojure 152 | (require '[clojurewerkz.cassaforte.client :as client] 153 | '[clojurewerkz.cassanforte.cql :as cql]) 154 | 155 | (def my-prepared-statement 156 | (client/prepare (insert s :users {:name ? 157 | :city ? 158 | :age ?}))) 159 | 160 | (client/execute session 161 | (client/bind my-prepared-statement ["Alex" "Munich" (int 19)])) 162 | ``` 163 | 164 | Alternatively, you can use string queries in prepare: 165 | 166 | ```clojure 167 | (def my-prepared-statement 168 | (client/prepare session "INSERT INTO users (name, city, age) VALUES (?, ?, ?);")) 169 | ``` 170 | 171 | Old Prepared Statment API is deprecated. 172 | 173 | ### Async Queries 174 | 175 | As earlier, you can keep using default async commands: `insert-async`, `update-async` 176 | `delete-async`, `select-async`. In addition, you can use `clojurewerkz.cassaforte.client/async` 177 | macro to execute any query asyncronously. 178 | 179 | In addition, you can add a listener, which will be called whenever the query is done 180 | ```clojure 181 | (let [result-future (select-async s :users)] 182 | (client/add-listener result-future 183 | (fn [] (println "DONE! Result: " @result-future)) 184 | (Executors/newFixedThreadPool 1)) 185 | @result-future) 186 | ``` 187 | 188 | Also, you now can use `deref` operation and specify timeout: 189 | 190 | ```clojure 191 | (deref (select-async s :users) 1 java.util.concurrent.TimeUnit/SECONDS) 192 | ``` 193 | 194 | ### Retry Policy and Consistency Level overrides 195 | 196 | You can override `retry-policy` and `consistency-level` for each query you run: 197 | 198 | ```clojure 199 | (require '[clojurewerkz.cassaforte.policies :as policies]) 200 | 201 | (client/execute session 202 | (client/build-statement "SELECT * FROM users") 203 | :retry-policy (:downgrading-consistency policies/retry-policies) 204 | :consistency-level (:any policies/consistency-levels)) 205 | ``` 206 | 207 | You __have to__ build the statement manually for that (usually it's done under the hood), 208 | or use prepared statements (advised). 209 | 210 | ### clojurewerkz.cassanforte.cql/copy-table 211 | 212 | `clojurewerkz.cassanforte.cql/copy-table` is a new function that 213 | copies all rows from one table to another, applying a transforming 214 | function (`clojure.core/identity` by default): 215 | 216 | ``` clojure 217 | (require '[clojurewerkz.cassanforte.cql :as cql]) 218 | 219 | ;; copies all rows from people to people2, using clojure.core/identity 220 | ;; to transform rows, 16384 rows at a time 221 | (cql/copy-table session "people" "people2" :id identity 16384) 222 | ``` 223 | 224 | This function is primarily helpful when migration Cassandra 225 | schema but can also be useful in test environments. 226 | 227 | ### Cluster Resource Leak Plugged 228 | 229 | The client now properly releases all resources 230 | associated with cluster connection(s) and state. 231 | 232 | Contributed by Philip Doctor (DataStax). 233 | 234 | 235 | ### DataStax Java Driver Update 236 | 237 | DataStax Java driver has been updated to `2.1.4`. 238 | 239 | 240 | ## Changes between 2.0.0-rc5 and 2.0.0 241 | 242 | There were no code changes in 2.0 GA. The project 243 | now includes Apache Software License 2.0 headers. 244 | License files for both APL2 and EPL1 are included 245 | in the distribution. 246 | 247 | 248 | ## Changes between 2.0.0-rc4 and 2.0.0-rc5 249 | 250 | ### Correct Deserialisation of Empty Strings 251 | 252 | Empty string values are now correctly deserialised (previously they 253 | were returned as `nil`). 254 | 255 | GH issue: [#91](https://github.com/clojurewerkz/cassaforte/issues/91). 256 | 257 | 258 | ## Changes between 2.0.0-rc3 and 2.0.0-rc4 259 | 260 | ### Cassandra Native Protocol v2 as Default 261 | 262 | To preserve Cassandra 2.0 compatibility yet continue using the most recent Cassandra Java driver 263 | Cassaforte now uses native protocol v2 by default. v3 can be opted into 264 | using the `:protocol-version` connection option (with value of `3`). 265 | 266 | 267 | ### Hayt 2.0 268 | 269 | Hayt dependency has been upgraded to `2.0` (GA). 270 | 271 | 272 | ## Changes between 2.0.0-rc2 and 2.0.0-rc3 273 | 274 | ### Fetch Size Support 275 | 276 | (Internal to the client) automatic paging of result set rows now can be configured 277 | or disabled altogether, e.g. when running into problems similar to [CASSANDRA-6722](https://issues.apache.org/jira/browse/CASSANDRA-6722). 278 | 279 | `clojurewerkz.cassaforte.client/with-fetch-size` is a macro that does that: 280 | 281 | ``` clojure 282 | (require '[clojurewerkz.cassaforte.client :as cc]) 283 | 284 | ;; alter page size 285 | (cc/with-fetch-size 8192 286 | (comment "SELECT queries go here")) 287 | 288 | ;; disable internal client paging 289 | (cc/with-fetch-size Integer/MAX_VALUE 290 | (comment "SELECT queries go here")) 291 | ``` 292 | 293 | Default fetch size is unaltered (Cassaforte relies on the Java driver default). This setting 294 | only makes sense for a certain subset of `SELECT` queries. 295 | 296 | 297 | 298 | ## Changes between 2.0.0-rc1 and 2.0.0-rc2 299 | 300 | ### Fixes Race Condition in Async Operations 301 | 302 | Async database operations no longer suffer from a race condition between 303 | issueing them and definiting callbacks on the returned future value. 304 | 305 | Contributed by Kirill Chernyshov. 306 | 307 | ### Compression Option 308 | 309 | `:compression` is a new option that can be used when connecting: 310 | 311 | ``` clojure 312 | (require '[clojurewerkz.cassaforte.client :as client]) 313 | 314 | (let [s (client/connect ["127.0.0.1"] "my-keyspace" {:compression :snappy})] 315 | ) 316 | ``` 317 | 318 | Valid compression values are: 319 | 320 | * `:snappy` 321 | * `:lz4` 322 | * `:none` (or `nil`) 323 | 324 | Contirbuted by Max Barnash (DataStax). 325 | 326 | 327 | ## Changes between 2.0.0-beta9 and 2.0.0-rc1 328 | 329 | ### Clojure 1.4 and 1.5 Support Dropped 330 | 331 | Cassaforte now requires Clojure `1.6.0`. 332 | 333 | 334 | ## Changes between 2.0.0-beta8 and 2.0.0-beta9 335 | 336 | ### Collections Converted to Clojure Data Structures 337 | 338 | Cassandra maps, sets and lists are now automatically converted to their 339 | immutable Clojure counterparts. 340 | 341 | 342 | ### Atomic Batches Support 343 | 344 | [Atomic batches](http://www.datastax.com/documentation/cql/3.1/cql/cql_reference/batch_r.html) are now easier to use with Cassaforte: 345 | 346 | ``` clojure 347 | (require '[clojurewerkz.cassaforte.client :as client]) 348 | (require '[clojurewerkz.cassaforte.cql :as cql :refer :all]) 349 | (require '[clojurewerkz.cassaforte.query :refer :all]) 350 | (require '[qbits.hayt.dsl.statement :as hs]) 351 | 352 | (let [s (client/connect ["127.0.0.1"])] 353 | (cql/atomic-batch s (queries 354 | (hs/insert :users (values {:name "Alex" :city "Munich" :age (int 19)})) 355 | (hs/insert :users (values {:name "Fritz" :city "Hamburg" :age (int 28)}))))) 356 | ``` 357 | 358 | 359 | ## Changes between 2.0.0-beta7 and 2.0.0-beta8 360 | 361 | `2.0.0-beta8` introduces a major **breaking API change**. 362 | 363 | ### Query DSL Taken From Hayt 2.0 364 | 365 | Cassaforte no longer tries to support query condition DSLs for both Hayt 1.x 366 | and Hayt 2.0. Hayt 2.0 is the only supported flavour now and 367 | is the future. 368 | 369 | Some examples of the changes: 370 | 371 | ``` clojure 372 | ;; before 373 | (where :name "Alex") 374 | 375 | ;; after 376 | (where [[= :name "Alex"]]) 377 | (where {:name "Alex"}) 378 | 379 | 380 | ;; before 381 | (where :name "Alex" :city "Munich") 382 | 383 | ;; after 384 | (where [[= :name "Alex"] 385 | [= :city "Munich"]]) 386 | (where {:name "Alex" :city "Munich"}) 387 | 388 | 389 | ;; before 390 | (where :name "Alex" :age [> 25]) 391 | 392 | ;; after 393 | (where [[= :name "Alex"] 394 | [> :age 25]]) 395 | 396 | ;; before 397 | (where :name "Alex" :city [:in ["Munich" "Frankfurt"]]) 398 | 399 | ;; after 400 | (where [[= :name "Alex"] 401 | [:in :city ["Munich" "Frankfurt"]]]) 402 | ``` 403 | 404 | As it's easy to see, the new condition style closer resembles 405 | Clojure itself and thus was a reasonable decision on behalf of Hayt 406 | developers. 407 | 408 | 409 | ## Changes between 2.0.0-beta5 and 2.0.0-beta7 410 | 411 | ### Hayt Upgraded to 2.0 412 | 413 | [Hayt](https://github.com/mpenet/hayt) was upgraded to 2.0. 414 | 415 | 416 | ## Changes between 2.0.0-beta4 and 2.0.0-beta5 417 | 418 | ### clojurewerkz.cassandra.cql/iterate-table Now Terminates 419 | 420 | `clojurewerkz.cassandra.cql/iterate-table` no longer produces an infinite 421 | sequence. 422 | 423 | 424 | ### Keyspace as Option 425 | 426 | It is now possible to choose keyspace via an option: 427 | 428 | ``` clojure 429 | (ns cassaforte.docs 430 | (:require [clojurewerkz.cassaforte.client :as cc])) 431 | 432 | (let [conn (cc/connect {:hosts ["127.0.0.1"] :keyspace "a-keyspace"})] 433 | ) 434 | ``` 435 | 436 | Contributed by Max Barnash (DataStax). 437 | 438 | 439 | 440 | ## Changes between 2.0.0-beta3 and 2.0.0-beta4 441 | 442 | ### URI Connections 443 | 444 | It is now possible to connect to a node and switch to a namespace 445 | using a URI string: 446 | 447 | ``` clojure 448 | (ns cassaforte.docs 449 | (:require [clojurewerkz.cassaforte.client :as cc])) 450 | 451 | ;; connects to node 127.0.0.1:9042 and uses "new_cql_keyspace" as keyspace 452 | (cc/connect-with-uri "cql://127.0.0.1:9042/new_cql_keyspace") 453 | ``` 454 | 455 | 456 | 457 | ## Changes between 2.0.0-beta2 and 2.0.0-beta3 458 | 459 | ### Cassandra 2.1 Compatibility 460 | 461 | Cassaforte 2.0 is compatible with Cassandra 2.1. 462 | 463 | 464 | ### Prepared Statement Cache Removed 465 | 466 | Prepared statement cache was affecting client correctness in some cases 467 | and was removed. 468 | 469 | 470 | ### Clojure 1.7.0-alpha2+ Compatibility 471 | 472 | Cassaforte is now compatible with Clojure `1.7.0-alpha2` and later versions. 473 | 474 | 475 | 476 | ## Changes between 1.3.0 and 2.0.0-beta2 477 | 478 | Cassaforte 2.0 has [breaking API changes](http://blog.clojurewerkz.org/blog/2014/04/26/major-breaking-public-api-changes-coming-in-our-projects/) in most namespaces. 479 | 480 | ### Client (Session) is Explicit Argument 481 | 482 | All Cassaforte public API functions that issue requests to Cassandra now require 483 | a client (session) to be passed as an explicit argument: 484 | 485 | ```clj 486 | (ns cassaforte.docs 487 | (:require [clojurewerkz.cassaforte.client :as cc] 488 | [clojurewerkz.cassaforte.cql :as cql])) 489 | 490 | (let [conn (cc/connect ["127.0.0.1"])] 491 | (cql/use-keyspace conn "cassaforte_keyspace")) 492 | ``` 493 | 494 | ```clj 495 | (ns cassaforte.docs 496 | (:require [clojurewerkz.cassaforte.client :as cc] 497 | [clojurewerkz.cassaforte.cql :as cql] 498 | [clojurewerkz.cassaforte.query :refer :all])) 499 | 500 | (let [conn (cc/connect ["127.0.0.1"])] 501 | (cql/create-table conn "user_posts" 502 | (column-definitions {:username :varchar 503 | :post_id :varchar 504 | :body :text 505 | :primary-key [:username :post_id]}))) 506 | ``` 507 | 508 | ```clj 509 | (ns cassaforte.docs 510 | (:require [clojurewerkz.cassaforte.client :as cc] 511 | [clojurewerkz.cassaforte.cql :as cql])) 512 | 513 | (let [conn (cc/connect ["127.0.0.1"])] 514 | (cql/insert conn "users" {:name "Alex" :age (int 19)})) 515 | ``` 516 | 517 | ### Policy Namespace 518 | 519 | Policy-related functions from `clojurewerkz.cassaforte.client` were extracted into 520 | `clojurewerkz.cassaforte.policies`: 521 | 522 | ```clojure 523 | (require '[clojurewerkz.cassaforte.policies :as cp]) 524 | 525 | (cp/exponential-reconnection-policy 100 1000) 526 | ``` 527 | 528 | ``` clojure 529 | (require '[clojurewerkz.cassaforte.policies :as cp]) 530 | 531 | (let [p (cp/round-robin-policy)] 532 | (cp/token-aware-policy p)) 533 | ``` 534 | 535 | ### DataStax Java Driver Update 536 | 537 | DataStax Java driver has been updated to `2.1.x`. 538 | 539 | 540 | ### Cassandra Sessions Compatible with with-open 541 | 542 | `Session#shutdown` was renamed to `Session#close` in 543 | cassandra-driver-core. Cassaforte needs to be adapted to that. 544 | 545 | Contributed by Jarkko Mönkkönen. 546 | 547 | ### TLS and Kerberos Support 548 | 549 | Cassaforte now supports TLS connections and Kerberos authentication 550 | via [DataStax CQL extensions](http://www.datastax.com/dev/blog/accessing-secure-dse-clusters-with-cql-native-protocol). 551 | 552 | The `:ssl` connection option now can be a map with two keys: 553 | 554 | * `:keystore-path` 555 | * `:keystore-password` 556 | 557 | which provide a path and password to a [JDK KeyStore](http://docs.oracle.com/javase/7/docs/api/java/security/KeyStore.html) 558 | on disk, created with 559 | [keytool](http://docs.oracle.com/javase/7/docs/technotes/tools/solaris/keytool.html). 560 | 561 | Optionally, an instance of 562 | [SSLOptions](http://www.datastax.com/drivers/java/2.0/com/datastax/driver/core/SSLOptions.html) 563 | can be provided via the `:ssl-options` connection option. 564 | 565 | Contributed by Max Barnash. 566 | 567 | GH issue: [#60](https://github.com/clojurewerkz/cassaforte/pull/60). 568 | 569 | ### Support for overriding default SSL cipher suites 570 | 571 | Providing a `:cipher-suites` key in the `:ssl` connection option allows to specify cipher suites 572 | that are enabled when connecting to a cluster with SSL. 573 | The value of this key is a Seq of Strings (e.g. a vector) where each item specifies a cipher suite: 574 | 575 | ```clj 576 | (ns cassaforte.docs 577 | (:require [clojurewerkz.cassaforte.client :as cc])) 578 | 579 | (cc/build-cluster {:ssl {:keystore-path "path/to/keystore" 580 | :keystore-password "password"}}) 581 | :cipher-suites] ["TLS_RSA_WITH_AES_128_CBC_SHA"]}} 582 | ``` 583 | 584 | The `:cipher-suites` key is optional and may be omitted, in which case Datastax Java driver's 585 | default cipher suites (`com.datastax.driver.core.SSLOptions/DEFAULT_SSL_CIPHER_SUITES`) are enabled. 586 | 587 | This can be used to work around the need to install Java Cryptography Extension (JCE) Unlimited Strength 588 | Jurisdiction Policy Files required by the default set of cipher suites. `TLS_RSA_WITH_AES_128_CBC_SHA` 589 | is a suite in the default set that works with the standard JCE. E.g. by specifying just that one, 590 | as in the code example, the standard JCE is enough. 591 | 592 | Contributed by Juhani Hietikko. 593 | 594 | GH issue: [#61](https://github.com/clojurewerkz/cassaforte/pull/61). 595 | 596 | ## Changes between 1.2.0 and 1.3.0 597 | 598 | ### Clojure 1.6 By Default 599 | 600 | The project now depends on `org.clojure/clojure` version `1.6.0`. It is 601 | still compatible with Clojure 1.4 and if your `project.clj` depends on 602 | a different version, it will be used, but 1.6 is the default now. 603 | 604 | We encourage all users to upgrade to 1.6, it is a drop-in replacement 605 | for the majority of projects out there. 606 | 607 | 608 | ### Cassandra Java Driver Update 609 | 610 | Cassandra Java driver has been updated to `2.0.x`. 611 | 612 | 613 | ### UUID Generation Helpers 614 | 615 | `clojurewerkz.cassaforte.uuids` is a new namespace that provides UUID 616 | generation helpers: 617 | 618 | ``` clojure 619 | (require '[clojurewerkz.cassaforte.uuids :as uuids]) 620 | 621 | (uuids/random) 622 | ;= #uuid "d43fdc16-a9c3-4d0f-8809-512115289537" 623 | 624 | (uuids/time-based) 625 | ;= #uuid "90cf6f40-4584-11e3-90c2-65c7571b1a52" 626 | 627 | (uuids/unix-timestamp (uuids/time-based)) 628 | ;= 1383592179743 629 | 630 | (u/start-of (u/unix-timestamp (u/time-based))) 631 | ;= #uuid "ad1fd130-4584-11e3-8080-808080808080" 632 | 633 | (u/end-of (u/unix-timestamp (u/time-based))) 634 | ;= #uuid "b31abb3f-4584-11e3-7f7f-7f7f7f7f7f7f" 635 | ``` 636 | 637 | ### Hayt Update 638 | 639 | Hayt dependency has been updated to `1.4.1`, which supports 640 | `if-not-exists` in `create-keyspace`: 641 | 642 | ``` clojure 643 | (create-keyspace "main" 644 | (if-not-exists) 645 | (with {:replication 646 | {:class "SimpleStrategy" 647 | :replication_factor 1 }})) 648 | ``` 649 | 650 | ### Extra Clauses Support in `insert-batch` 651 | 652 | It is now possible to use extra CQL clauses for every statement 653 | in a batch insert (e.g. to specify TTL): 654 | 655 | ``` clojure 656 | (cql/insert-batch "table" 657 | {:something "cats"} 658 | [{:something "dogs"} (using :ttl 60)]) 659 | ``` 660 | 661 | Contributed by Sam Neubardt. 662 | 663 | ### Alternative `where` syntax 664 | 665 | Now it is possible to specify hash in where clause, which makes queries 666 | more composable: 667 | 668 | ```clj 669 | (select :users 670 | (where {:city "Munich" 671 | :age [> (int 5)]}) 672 | (allow-filtering true)) 673 | ``` 674 | 675 | ### Batch Insert Improvements 676 | 677 | Clauses to be specified for each record in `insert-batch`: 678 | 679 | ``` clojure 680 | (let [input [[{:name "Alex" :city "Munich"} (using :ttl 350)] 681 | [{:name "Alex" :city "Munich"} (using :ttl 350)]]] 682 | (insert-batch th/session :users input)) 683 | ``` 684 | 685 | Contributed by Sam Neubardt. 686 | 687 | 688 | ## Changes between 1.1.0 and 1.2.0 689 | 690 | ### Cassandra Java Driver Update 691 | 692 | Cassandra Java driver has been updated to `1.0.3` which 693 | supports Cassandra 2.0. 694 | 695 | ### Fix problem with batched prepared statements 696 | 697 | `insert-batch` didn't play well with prepared statements, problem fixed now. You can use `insert-batch` 698 | normally with prepared statements. 699 | 700 | ### Hayt query generator update 701 | 702 | Hayt is updated to 1.1.3 version, which contains fixes for token function and some internal improvements 703 | that do not influence any APIs. 704 | 705 | ### Added new Consistency level DSL 706 | 707 | Consistency level can now be (also) passed as a symbol, without resolving it to ConsistencyLevel instance: 708 | 709 | ```clojure 710 | (client/with-consistency-level :quorum 711 | (insert :users r)) 712 | ``` 713 | 714 | Please note that old DSL still works and is supported. 715 | 716 | ### Password authentication supported 717 | 718 | Password authentication is now supported via the `:credentials` option to `client/build-cluster`. 719 | Give it a map with username and password: 720 | 721 | ```clojure 722 | (client/build-cluster {:contact-points ["127.0.0.1"] 723 | :credentials {:username "ceilingcat" :password "ohai"} 724 | ;; ... 725 | ``` 726 | 727 | Query DSL added for managing users `create-user`, `alter-user`, `drop-user`, `grant`, `revoke`, 728 | `list-users`, `list-permissions` for both multi and regular sessions. 729 | 730 | ## Changes between 1.0.0 and 1.1.0 731 | 732 | ### Fixes for prepared queries with multi-cql 733 | 734 | Multi-cql didn't work with unforced prepared statements, now it's possible to use 735 | `client/prepared` with multi-cql as well. 736 | 737 | ### Fixes for compound keys in iterate-world queries 738 | 739 | Iterate world didn't work fro tables with compound primary keys. Now it's possible 740 | to iterate over collections that have compound keys. 741 | 742 | ### Fixes for AOT Compilation 743 | 744 | Cassaforte now can be AOT compiled: `clojurewerkz.cassaforte.client/compile` 745 | is renamed back to `clojurewerkz.cassaforte.client/compile-query`. 746 | 747 | 748 | ## Changes between 1.0.0-rc5 and 1.0.0-rc6 749 | 750 | ### Raw Query Execution Improvements 751 | 752 | Raw (string) query execution is now easier to do. Low-level ops are now more explicit and easy to 753 | use. 754 | 755 | ### Underlying Java Driver update 756 | 757 | [Java Driver](https://github.com/datastax/java-driver) was updated to latest stable version, 1.0.1. 758 | 759 | ### Support of Clojure 1.4+ 760 | 761 | Hayt was been updated to [latest version (1.1.2)](https://github.com/mpenet/hayt/commit/c4ec6d8bea49843aaf0afa22a2cc09eff6fbc866), 762 | which allows Cassaforte support Clojure 1.4. 763 | 764 | 765 | ## Changes between 1.0.0-rc4 and 1.0.0-rc5 766 | 767 | `cassaforte.multi.cql` is a new namespace with functions that are very similar to those 768 | in the `cassaforte.cqll` namespace but always take a database reference as an explicit argument. 769 | 770 | They are supposed to be used in cases when Cassaforte's "main" API that uses an implicit var is not 771 | enough. 772 | 773 | 774 | ## Changes between 1.0.0-rc3 and 1.0.0-rc4 775 | 776 | `1.0.0-rc4` has **breaking API changes** 777 | 778 | ### Dependency Alia is Dropped 779 | 780 | Cassaforte no longer depends on Alia. 781 | 782 | ### Per-Statement Retry Policy and Consistency Level Settings 783 | 784 | `clojurewerkz.cassaforte.client/prepare` now accepts two more options: 785 | 786 | * `consistency-level` 787 | * `retry-policy` 788 | 789 | ### Namespace Reshuffling 790 | 791 | `clojurewerkz.cassaforte.cql/execute` is now `clojurewerkz.cassaforte.client/execute`, 792 | a few less frequently used functions were also moved between namespaces. 793 | 794 | 795 | ## Changes between 1.0.0-rc2 and 1.0.0-rc3 796 | 797 | * Update Hayt to latest version (1.0.0) 798 | * Update Cassandra to latest version (1.2.4) 799 | * Update java-driver to latest version (1.0.0) 800 | * Get rid of reflection warnings 801 | * Add more options for inspecing cluster 802 | * Improve debug output 803 | * Add helpers for wide columns 804 | * Simplify deserializations, remove serializaitons since they're handled by driver internally 805 | 806 | ## Changes between 1.0.0-beta1 and 1.0.0-rc2 807 | 808 | * Thrift support is discontinued 809 | * Use [Hayt](https://github.com/mpenet/hayt) for CQL generation 810 | * Update to java-driver 1.0.0-rc2 811 | * Significantly improved test coverage 812 | * Major changes in CQL API, vast majority of queries won't work anymore 813 | * Embedded server is used for tests and has it's own API now 814 | 815 | ## 1.0.0-beta1 816 | 817 | Initial release 818 | 819 | Supported features: 820 | 821 | * Connection to a single node 822 | * Create, destroy keyspace 823 | * CQL 3.0 queries, including queries with placeholders (?, a la JDBC) 824 | * Deserialization of column names and values according to response schema (not all types are supported yet) 825 | -------------------------------------------------------------------------------- /test/clojure/clojurewerkz/cassaforte/query_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojurewerkz.cassaforte.query-test 2 | (:import [com.datastax.driver.core ProtocolVersion] 3 | [com.datastax.driver.core.utils Bytes]) 4 | (:require [clojure.test :refer :all] 5 | [clojurewerkz.cassaforte.query :refer :all])) 6 | 7 | (def force-novalues #(.setForceNoValues % true)) 8 | 9 | (defn normalize-string 10 | [s] 11 | (-> s 12 | (.getQueryString) 13 | (clojure.string/replace "\t" " ") 14 | (clojure.string/replace #"^\n" "") 15 | (clojure.string/replace #"^\s+" ""))) 16 | 17 | (defmacro renders-to 18 | [query result] 19 | `(= ~query 20 | (-> ~result 21 | force-novalues 22 | normalize-string))) 23 | 24 | (deftest test-select-query 25 | (is (renders-to 26 | "SELECT asd FROM \"table-name\";" 27 | (select "table-name" 28 | (column "asd")))) 29 | 30 | (is (renders-to 31 | "SELECT * FROM foo;" 32 | (select (all) (from :foo)))) 33 | 34 | (is (renders-to 35 | "SELECT * FROM foo.bar;" 36 | (select (all) (from :foo :bar)))) 37 | 38 | (is (renders-to 39 | "SELECT first,second FROM \"table-name\";" 40 | (select "table-name" 41 | (column "first") 42 | (column "second")))) 43 | 44 | (is (renders-to "SELECT first,second FROM \"table-name\";" 45 | (select "table-name" 46 | (columns "first" "second")))) 47 | 48 | (is (renders-to "SELECT * FROM foo WHERE foo=1 AND bar=2;" 49 | (select :foo 50 | ;; Normally, hashmap can be used. Array map is used here to guarantee order. 51 | (where (array-map :foo 1 52 | :bar 2))))) 53 | (is (renders-to "SELECT * FROM foo WHERE foo='bar' AND moo>3 AND meh>4 AND baz IN (5,6,7);" 54 | (select :foo 55 | (where [[:= :foo "bar"] 56 | [:> :moo 3] 57 | [:> :meh 4] 58 | [:in :baz [5 6 7]] 59 | ])))) 60 | 61 | (is (renders-to "SELECT * FROM tv_series WHERE series_title='Futurama' LIMIT 10;" 62 | (select :tv_series 63 | (paginate :key :episode_id :per-page 10 64 | :where {:series_title "Futurama"})))) 65 | 66 | (is (renders-to "SELECT * FROM tv_series WHERE series_title='Futurama' AND episode_id>10 LIMIT 10;" 67 | (select :tv_series 68 | (paginate :key :episode_id :per-page 10 :last-key 10 69 | :where {:series_title "Futurama"})))) 70 | 71 | (is (renders-to "SELECT * FROM foo WHERE foo='bar' ORDER BY foo ASC;" 72 | (select :foo 73 | (order-by (asc :foo)) 74 | (where [[= :foo "bar"]])))) 75 | 76 | (is (renders-to "SELECT * FROM foo WHERE foo='bar' ORDER BY foo ASC LIMIT 10;" 77 | (select :foo 78 | (order-by (asc :foo)) 79 | (limit 10) 80 | (where [[= :foo "bar"]])))) 81 | 82 | (is (renders-to "SELECT * FROM foo WHERE foo='bar' ORDER BY foo ASC LIMIT 10 ALLOW FILTERING;" 83 | (select :foo 84 | (where [[= :foo "bar"]]) 85 | (order-by (asc :foo)) 86 | (limit 10) 87 | (allow-filtering) 88 | ))) 89 | 90 | (is (renders-to "SELECT * FROM foo WHERE foo='bar' ORDER BY foo ASC LIMIT 10 ALLOW FILTERING;" 91 | (select :foo 92 | (where [[= :foo "bar"]]) 93 | (order-by (asc :foo)) 94 | (limit 10) 95 | (allow-filtering true) 96 | ))) 97 | 98 | (is (renders-to "SELECT * FROM foo WHERE k=4 AND c>'a' AND c<='z';"; 99 | (select :foo 100 | (where [[= :k 4] 101 | [> :c "a"] 102 | [<= :c "z"]]) 103 | ))) 104 | 105 | (is (renders-to "SELECT DISTINCT asd AS bsd FROM foo;" 106 | (select :foo 107 | (columns (-> "asd" 108 | distinct* 109 | (as "bsd")))))) 110 | 111 | (is (renders-to "SELECT DISTINCT longName AS a,ttl(longName) AS ttla FROM foo LIMIT :limit;"; 112 | (select :foo 113 | (columns (-> "longName" 114 | distinct* 115 | (as "a")) 116 | (-> "longName" 117 | ttl-column 118 | (as "ttla"))) 119 | (limit (bind-marker "limit"))))) 120 | 121 | (is (renders-to "SELECT a,b,\"C\" FROM foo WHERE a IN ('127.0.0.1','127.0.0.3') AND \"C\"='foo' ORDER BY a ASC,b DESC LIMIT 42;"; 122 | (select :foo 123 | (columns "a" 124 | "b" 125 | (quote* "C")) 126 | (where [[:in :a ["127.0.0.1", "127.0.0.3"]] 127 | [:= (quote* "C") "foo"]]) 128 | (order-by (asc :a) 129 | (desc :b)) 130 | (limit (int 42))))) 131 | 132 | 133 | (is (renders-to "SELECT writetime(a),ttl(a) FROM foo ALLOW FILTERING;" 134 | (select :foo 135 | (columns (write-time :a) 136 | (ttl-column :a)) 137 | (allow-filtering)))) 138 | 139 | 140 | (is (renders-to "SELECT DISTINCT longName AS a,ttl(longName) AS ttla FROM foo WHERE k IN () LIMIT :limit;"; 141 | (select :foo 142 | (columns (-> "longName" 143 | (distinct*) 144 | (as "a")) 145 | (-> "longName" 146 | (ttl-column) 147 | (as "ttla"))) 148 | (where [[:in :k []]]) 149 | (limit (bind-marker "limit"))))) 150 | 151 | (is (renders-to "SELECT * FROM foo WHERE bar=:barmark AND baz=:bazmark LIMIT :limit;" 152 | (select :foo 153 | (where [[:= :bar (bind-marker "barmark")] 154 | [:= :baz (bind-marker "bazmark")]]) 155 | (limit (bind-marker "limit"))))) 156 | 157 | (is (renders-to "SELECT a FROM foo WHERE k IN ?;"; 158 | (select :foo 159 | (column :a) 160 | (where [[:in :k [?]]])))) 161 | 162 | (is (renders-to "SELECT count(*) FROM foo;"; 163 | (select :foo 164 | (count-all)))) 165 | 166 | (is (renders-to "SELECT intToBlob(b) FROM foo;" 167 | (select :foo 168 | (fcall "intToBlob" 169 | (cname "b"))))) 170 | 171 | (is (renders-to "SELECT unixTimestampOf(created_at) FROM events LIMIT 5;" 172 | (select :events 173 | (unix-timestamp-of :created_at) 174 | (limit 5)))) 175 | 176 | (is (renders-to "INSERT INTO events (created_at,message) VALUES (now(),'Message 1');" 177 | (insert :events 178 | {:created_at (now) 179 | :message "Message 1"}))) 180 | 181 | (is (renders-to "SELECT * FROM foo WHERE k>42 LIMIT 42;"; 182 | (select :foo 183 | (where [[:> :k 42]]) 184 | (limit 42)))) 185 | 186 | (is (renders-to "SELECT * FROM events WHERE message IN ('Message 7','Message 8','Message 9','Message 10') AND created_at>=minTimeuuid(1001);" 187 | (select :events 188 | (where [[:in :message ["Message 7" "Message 8" "Message 9" "Message 10"]] 189 | [>= :created_at (min-timeuuid 1001)]])))) 190 | 191 | (is (renders-to "SELECT * FROM foo WHERE token(k)>token(42);" 192 | (select :foo 193 | (where [[:> (token "k") (function-call "token" 42)]])))) 194 | 195 | (is (renders-to "SELECT * FROM foo2 WHERE token(a,b)>token(42,101);"; 196 | (select :foo2 197 | (where [[:> (token "a" "b") (function-call "token" 42 101)]])))) 198 | 199 | (is (renders-to "SELECT * FROM words WHERE w='):,ydL ;O,D';" 200 | (select :words 201 | (where {:w "):,ydL ;O,D"})))) 202 | 203 | (is (renders-to "SELECT * FROM words WHERE w='WA(!:gS)r(UfW';" 204 | (select :words 205 | (where {:w "WA(!:gS)r(UfW"})))) 206 | 207 | (is (renders-to "SELECT * FROM foo WHERE d=1234325;" 208 | (select :foo 209 | (where {:d (java.util.Date. 1234325)})))) 210 | 211 | (is (renders-to "SELECT * FROM foo WHERE b=0xcafebabe;" 212 | (select :foo 213 | (where {:b (Bytes/fromHexString "0xCAFEBABE")})))) 214 | 215 | (is (thrown? java.lang.IllegalStateException 216 | (select :foo 217 | (count-all) 218 | (order-by (asc "a") (desc "b")) 219 | (order-by (asc "a") (desc "b"))))) 220 | 221 | (is (thrown? java.lang.IllegalArgumentException 222 | (select :foo 223 | (limit -42)))) 224 | 225 | (is (thrown? java.lang.IllegalStateException 226 | (select :foo 227 | (limit 42) 228 | (limit 42))))) 229 | 230 | (deftest test-insert-query 231 | (is (renders-to "INSERT INTO foo (asd) VALUES ('bsd');" 232 | (insert :foo 233 | {"asd" "bsd"}))) 234 | (is (renders-to "INSERT INTO foo (asd) VALUES ('bsd');" 235 | (insert :foo 236 | {"asd" "bsd"}))) 237 | 238 | (is (renders-to "INSERT INTO foo (asd) VALUES ('bsd');" 239 | (insert :foo 240 | {"asd" "bsd"}))) 241 | 242 | 243 | (is (renders-to "INSERT INTO foo (a,b,\"C\",d) VALUES (123,'127.0.0.1','foo''bar',{'x':3,'y':2}) USING TIMESTAMP 42 AND TTL 24;" 244 | (insert :foo 245 | (array-map "a" 123 246 | "b" (java.net.InetAddress/getByName "127.0.0.1") 247 | (quote* "C") "foo'bar" 248 | "d" (array-map "x" 3 "y" 2)) 249 | (using :timestamp 42 250 | :ttl 24)))) 251 | 252 | (is (renders-to "INSERT INTO foo (a,b,\"C\",d) VALUES (123,'127.0.0.1','foo''bar',{'x':3,'y':2}) USING TIMESTAMP 42 AND TTL 24;" 253 | (insert :foo 254 | (array-map "a" 123 255 | "b" (java.net.InetAddress/getByName "127.0.0.1") 256 | (quote* "C") "foo'bar" 257 | "d" {"x" 3 "y" 2}) 258 | (using :timestamp 42 259 | :ttl 24)))) 260 | 261 | (is (renders-to "INSERT INTO foo (a,b) VALUES ({2,3,4},3.4) USING TIMESTAMP 42 AND TTL 24;" 262 | (insert :foo 263 | (array-map "a" (sorted-set 2 3 4) 264 | "b" 3.4) 265 | (using :timestamp 42 266 | :ttl 24)))) 267 | 268 | (is (renders-to "INSERT INTO foo (a,b) VALUES ({2,3,4},3.4) USING TTL ? AND TIMESTAMP ?;" 269 | (insert :foo 270 | (array-map :a (sorted-set 2 3 4) 271 | :b 3.4) 272 | (using :ttl ? 273 | :timestamp ?)))) 274 | 275 | (is (renders-to "INSERT INTO foo (c,a,b) VALUES (123,{2,3,4},3.4) USING TIMESTAMP 42;" 276 | (insert :foo 277 | (array-map :c 123 278 | :a (sorted-set 2 3 4) 279 | :b 3.4) 280 | (using :timestamp 42)))) 281 | 282 | (is (renders-to "INSERT INTO foo (k,x) VALUES (0,1) IF NOT EXISTS;"; 283 | (insert :foo 284 | (array-map :k 0 285 | :x 1) 286 | (if-not-exists)))) 287 | 288 | 289 | (is (renders-to "INSERT INTO foo (k,x) VALUES (0,(1));"; 290 | (insert :foo 291 | (array-map :k 0 292 | :x (tuple-of (ProtocolVersion/fromInt 3) [:int] [(int 1)])))))) 293 | 294 | (deftest update-test 295 | (is (renders-to "UPDATE foo USING TIMESTAMP 42 SET a=12,b=[3,2,1],c=c+3 WHERE k=2;" 296 | (update :foo 297 | (array-map :a 12 298 | :b [3 2 1] 299 | :c (increment-by 3)) 300 | (where {:k 2}) 301 | (using :timestamp 42)))) 302 | 303 | 304 | (is (renders-to "UPDATE foo SET b=null WHERE k=2;" 305 | (update :foo 306 | (array-map :b nil) 307 | (where {:k 2})))) 308 | 309 | 310 | (is (renders-to "UPDATE foo SET a[2]='foo',b=[3,2,1]+b,c=c-{'a'} WHERE k=2 AND l='foo' AND m<4 AND n>=1;" 311 | (update :foo 312 | (array-map :a (set-idx 2 "foo") 313 | :b (prepend-all [3,2,1]) 314 | :c (remove-tail "a")) 315 | (where [[= :k 2] 316 | [= :l "foo"] 317 | [< :m 4] 318 | [>= :n 1]])))) 319 | 320 | 321 | (is (renders-to "UPDATE foo SET b=[3]+b,c=c+['a'],d=d+[1,2,3],e=e-[1];" 322 | (update :foo 323 | (array-map :b (prepend 3) 324 | :c (append "a") 325 | :d (append-all [1 2 3]) 326 | :e (discard 1))))) 327 | 328 | (is (renders-to "UPDATE foo SET b=b-[1,2,3],c=c+{1},d=d+{4,3,2};" 329 | (update :foo 330 | (array-map :b (discard-all [1 2 3]) 331 | :c (add-tail 1) 332 | :d (add-all-tail #{2 3 4}))))) 333 | 334 | (is (renders-to "UPDATE foo SET b=b-{2,3,4},c['k']='v',d=d+{'x':3,'y':2};" 335 | (update :foo 336 | (array-map :b (remove-all-tail (sorted-set 2 3 4)) 337 | :c (put-value "k" "v") 338 | :d (put-values (array-map "x" 3 339 | "y" 2)))))) 340 | 341 | (is (renders-to "UPDATE foo USING TTL 400;" 342 | (update :foo 343 | {} 344 | (using :ttl 400)))) 345 | 346 | 347 | (is (renders-to (str "UPDATE foo SET a=" (BigDecimal. 3.2) ",b=42 WHERE k=2;") 348 | (update :foo 349 | (array-map :a (BigDecimal. 3.2) 350 | :b (BigInteger. "42")) 351 | (where {:k 2})))) 352 | 353 | (is (renders-to "UPDATE foo USING TIMESTAMP 42 SET b=[3,2,1]+b WHERE k=2 AND l='foo';" 354 | (update :foo 355 | (array-map :b (prepend-all [3 2 1])) 356 | (where (array-map :k 2 357 | :l "foo")) 358 | (using :timestamp 42)))) 359 | 360 | (is (thrown? IllegalArgumentException 361 | (update :foo 362 | {} 363 | (using :ttl -400)))) 364 | 365 | (is (renders-to "UPDATE foo SET x=4 WHERE k=0 IF x=1;" 366 | (update :foo 367 | {:x 4} 368 | (where {:k 0}) 369 | (only-if {:x 1})))) 370 | 371 | (is (renders-to "UPDATE foo SET x=4 WHERE k=0 IF foo='bar' AND moo>3 AND meh>4 AND baz IN (5,6,7);" 372 | (update :foo 373 | {:x 4} 374 | (where {:k 0}) 375 | (only-if [[:= :foo "bar"] 376 | [:> :moo 3] 377 | [:> :meh 4] 378 | [:in :baz [5 6 7]] 379 | ]))))) 380 | 381 | (deftest test-delete 382 | (is (renders-to "DELETE a,b,c FROM foo;" 383 | (delete :foo 384 | (columns :a :b :c)))) 385 | 386 | (is (renders-to "DELETE a,b,c FROM foo WHERE k=0;" 387 | (delete :foo 388 | (columns :a :b :c) 389 | (where {:k 0})))) 390 | 391 | (is (renders-to "DELETE a[3],b['foo'],c FROM foo WHERE k=1;" 392 | (delete :foo 393 | (columns (list-elt :a 3) 394 | (map-elt :b "foo") 395 | :c) 396 | (where {:k 1})))) 397 | 398 | (is (renders-to "DELETE FROM foo USING TIMESTAMP 1240003134 WHERE k='value';" 399 | (delete :foo 400 | (using :timestamp 1240003134) 401 | (where {:k "value"})))) 402 | 403 | (is (renders-to "DELETE FROM foo WHERE k1='foo' IF EXISTS;" 404 | (delete :foo 405 | (where {:k1 "foo"}) 406 | (if-exists)))) 407 | 408 | (is (renders-to "DELETE FROM foo WHERE k1='foo' IF a=1 AND b=2;" 409 | (delete :foo 410 | (where {:k1 "foo"}) 411 | (only-if (array-map :a 1 412 | :b 2))))) 413 | 414 | (is (renders-to "DELETE FROM foo WHERE k1=:key;" 415 | (delete :foo 416 | (where {:k1 (bind-marker "key")}))))) 417 | 418 | 419 | (deftest test-truncate 420 | (is (renders-to "TRUNCATE a;" 421 | (truncate :a))) 422 | 423 | (is (renders-to "TRUNCATE b.a;" 424 | (truncate :a :b)))) 425 | 426 | (deftest test-batch 427 | (is (renders-to (str "BEGIN BATCH INSERT INTO foo (asd) VALUES ('bsd');" 428 | "INSERT INTO foo (asd) VALUES ('bsd');" 429 | "INSERT INTO foo (asd) VALUES ('bsd');" 430 | "APPLY BATCH;") 431 | (batch 432 | (queries 433 | (insert :foo 434 | {"asd" "bsd"}) 435 | (insert :foo 436 | {"asd" "bsd"}) 437 | (insert :foo 438 | {"asd" "bsd"}))))) 439 | 440 | (is (renders-to (str "BEGIN COUNTER BATCH USING TIMESTAMP 42 " 441 | "UPDATE foo SET a=a+1;" 442 | "UPDATE foo SET b=b+1;" 443 | "UPDATE foo SET c=c+1;" 444 | "APPLY BATCH;") 445 | (batch 446 | (using :timestamp 42) 447 | (queries 448 | (update :foo 449 | (array-map :a (increment-by 1))) 450 | (update :foo 451 | (array-map :b (increment-by 1))) 452 | (update :foo 453 | (array-map :c (increment-by 1)))))))) 454 | 455 | (deftest test-create-table 456 | (is (= "CREATE TABLE foo( 457 | a int, 458 | b varchar, 459 | c int, 460 | d int, 461 | e int, 462 | PRIMARY KEY((a, b), c, d))" 463 | (normalize-string 464 | (create-table :foo 465 | (column-definitions (array-map :a :int 466 | :b :varchar 467 | :c :int 468 | :d :int 469 | :e :int 470 | :primary-key [[:a :b] :c :d])))))) 471 | 472 | (is (= "CREATE TABLE foo( 473 | a int, 474 | c int, 475 | d int, 476 | b varchar, 477 | e int, 478 | PRIMARY KEY(a, c, d))" 479 | (normalize-string 480 | (create-table :foo 481 | (column-definitions (array-map :a :int 482 | :c :int 483 | :d :int 484 | :b :varchar 485 | :e :int 486 | :primary-key [:a :c :d])))))) 487 | 488 | 489 | (is (= "CREATE TABLE foo( 490 | a varchar, 491 | b varchar, 492 | c varchar, 493 | d varchar, 494 | PRIMARY KEY((a, b), c, d)) 495 | WITH COMPACT STORAGE" 496 | (normalize-string 497 | (create-table :foo 498 | (column-definitions (array-map :a :varchar 499 | :b :varchar 500 | :c :varchar 501 | :d :varchar 502 | :primary-key [[:a :b] :c :d])) 503 | (with {:compact-storage true}) 504 | )))) 505 | 506 | (is (= "CREATE TABLE foo( 507 | a int, 508 | b map, 509 | PRIMARY KEY(a))" 510 | (normalize-string 511 | (create-table :foo 512 | (column-definitions (array-map :a :int 513 | :b (map-type :varchar :varchar) 514 | :primary-key [:a])))))) 515 | 516 | (is (= "CREATE TABLE IF NOT EXISTS foo( 517 | a int, 518 | b varchar, 519 | PRIMARY KEY(a))" 520 | (normalize-string 521 | (create-table :foo 522 | (column-definitions (array-map :a :int 523 | :b :varchar 524 | :primary-key [:a])) 525 | (if-not-exists) 526 | ))))) 527 | 528 | (deftest test-alter-table 529 | (is (= "ALTER TABLE foo ALTER foo TYPE int" 530 | (normalize-string 531 | (alter-table :foo 532 | (alter-column :foo :int))))) 533 | 534 | (is (= "ALTER TABLE foo ADD bar varchar" 535 | (normalize-string 536 | (alter-table :foo 537 | (add-column :bar :varchar))))) 538 | 539 | (is (= "ALTER TABLE foo RENAME baz TO ban" 540 | (normalize-string 541 | (alter-table :foo 542 | (rename-column :baz :ban))))) 543 | 544 | (is (= "ALTER TABLE foo DROP bar" 545 | (normalize-string 546 | (alter-table :foo 547 | (drop-column :bar))))) 548 | 549 | (is (= "ALTER TABLE foo ALTER foo TYPE int" 550 | (normalize-string 551 | (alter-table :foo 552 | (alter-column :foo :int) 553 | (with {:default-ttl 100})))))) 554 | 555 | (deftest test-drop-table 556 | (is (= "DROP TABLE foo" 557 | (normalize-string 558 | (drop-table :foo))))) 559 | 560 | (deftest test-create-index 561 | (is (= "CREATE INDEX foo ON bar(baz)" 562 | (normalize-string 563 | (create-index "foo" 564 | (on-table "bar") 565 | (and-column "baz"))))) 566 | 567 | (is (= "CREATE INDEX foo ON bar(KEYS(baz))" 568 | (normalize-string 569 | (create-index "foo" 570 | (on-table "bar") 571 | (and-keys-of-column "baz")))))) 572 | 573 | (deftest test-drop-keyspace 574 | (is (= "DROP KEYSPACE foo;" 575 | (normalize-string 576 | (drop-keyspace "foo")))) 577 | (is (= "DROP KEYSPACE IF EXISTS foo;" 578 | (normalize-string 579 | (drop-keyspace "foo" (if-exists)))))) 580 | 581 | (deftest test-create-keyspace 582 | (is (= "CREATE KEYSPACE IF NOT EXISTS foo;" 583 | (normalize-string 584 | (create-keyspace "foo" 585 | (if-not-exists))))) 586 | 587 | (is (= "CREATE KEYSPACE IF NOT EXISTS foo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};" 588 | (normalize-string 589 | (create-keyspace "foo" 590 | (with 591 | {:replication 592 | {:class "SimpleStrategy" 593 | :replication_factor 1}}) 594 | (if-not-exists)))))) 595 | 596 | (deftest test-alter-keyspace 597 | (is (= "ALTER KEYSPACE new_cql_keyspace WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 1, 'dc2': 2} AND DURABLE_WRITES = false;" 598 | (normalize-string 599 | (alter-keyspace "new_cql_keyspace" 600 | (with {:durable-writes false 601 | :replication (array-map "class" "NetworkTopologyStrategy" 602 | "dc1" 1 603 | "dc2" 2)}))))) 604 | 605 | 606 | (is (= "ALTER KEYSPACE foo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};" 607 | (normalize-string 608 | (alter-keyspace "foo" 609 | (with 610 | {:replication 611 | {"class" "SimpleStrategy" 612 | "replication_factor" 1}})))))) 613 | --------------------------------------------------------------------------------