diff --git a/.gitignore b/.gitignore index d30f3fc79..98ec99e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ /venv /lib/cassandra_murmur3.* /node_modules +/*.gem +/.bundle/* \ No newline at end of file diff --git a/.nav b/.nav new file mode 100644 index 000000000..55921b5e3 --- /dev/null +++ b/.nav @@ -0,0 +1,2 @@ +features +api diff --git a/.rubocop.yml b/.rubocop.yml index 29bad472e..76236d69f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,13 +2,15 @@ inherit_from: .rubocop_todo.yml #### The following group of customizations is questionable. #### -# We currently exclude types.rb and fetchers.rb because auto-correct results in an -# infinite loop in RuboCop. AllCops: TargetRubyVersion: 1.9 Exclude: - - 'lib/cassandra/types.rb' - - 'lib/cassandra/cluster/schema/fetchers.rb' + - 'spec/**/*' + - 'benchmarking/**/*' + - 'support/**/*' + - 'integration/**/*' + - 'features/**/*' + - 'tmp/**/*' # We shouldn't rescue Exception! Lint/RescueException: @@ -25,26 +27,42 @@ Metrics/ModuleLength: Metrics/ClassLength: Enabled: false +Metrics/ParameterLists: + Enabled: false + +Style/MultilineBlockLayout: + Enabled: false + Style/MultilineTernaryOperator: Enabled: false +Style/PredicateName: + NamePrefixBlacklist: is_, have_ + NameWhitelist: is_a? + Style/SingleLineBlockParams: Enabled: false Style/SignalException: EnforcedStyle: only_raise +Style/TrivialAccessors: + Enabled: false + Lint/EndAlignment: AutoCorrect: true Style/IfUnlessModifier: - MaxLineLength: 90 + MaxLineLength: 120 + +Style/RaiseArgs: + Enabled: false Style/WhileUntilModifier: - MaxLineLength: 90 + MaxLineLength: 120 Metrics/LineLength: - Max: 90 + Max: 120 # To make it possible to copy or click on URIs in the code, we allow lines # contaning a URI to be longer than Max. AllowHeredoc: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 81ee43c38..a89afb42a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -190,29 +190,6 @@ Style/OpMethod: - 'lib/cassandra/protocol/requests/query_request.rb' - 'lib/cassandra/protocol/responses/ready_response.rb' -# Offense count: 14 -# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. -# NamePrefix: is_, has_, have_ -# NamePrefixBlacklist: is_, has_, have_ -# NameWhitelist: is_a? -Style/PredicateName: - Exclude: - - 'lib/cassandra/cluster/registry.rb' - - 'lib/cassandra/cluster/schema.rb' - - 'lib/cassandra/function.rb' - - 'lib/cassandra/keyspace.rb' - - 'lib/cassandra/load_balancing.rb' - - 'lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb' - - 'lib/cassandra/load_balancing/policies/round_robin.rb' - - 'lib/cassandra/load_balancing/policies/token_aware.rb' - - 'lib/cassandra/table.rb' - - 'lib/cassandra/udt.rb' - -# Offense count: 2 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: compact, exploded -#Style/RaiseArgs: -# Enabled: false # Offense count: 46 # Cop supports --auto-correct. diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a6549d2..fee81e140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,81 @@ # master + +# 3.2.0 + Features: +* [RUBY-294](https://datastax-oss.atlassian.net/browse/RUBY-294) Support MRI 2.4.x. Thanks, @lautis, for this contribution! Bug Fixes: -* [RUBY-161] Protocol version negotiation in mixed version clusters should not fall back to v1 unless it is truly warranted. +* [RUBY-291](https://datastax-oss.atlassian.net/browse/RUBY-291) Driver fails to connect to cluster when a table column type has a quoted name. +* [RUBY-292](https://datastax-oss.atlassian.net/browse/RUBY-292) Driver sporadically crashes with "undefined method 'ip'" error. +Thanks, @grosser, for the fix! +* [RUBY-295](https://datastax-oss.atlassian.net/browse/RUBY-295) When a custom address resolver is configured, +consult it when handling all host events, and thus prevent the creation of invalid Host objects. + +# 3.1.0 +Features: +* Do not mark a host as down if there are active connections. +* Update Keyspace metadata to include collection of indexes defined in the keyspace. +* Update Table metadata to include trigger-collection and view-collection metadata. Also include the cdc attribute, + introduced in C* 3.8. More details [here.](http://cassandra.apache.org/doc/latest/operating/cdc.html) +* Added execution profiles to encapsulate a group of request execution options. +* Added support for v5 beta protocol. This will always be a "work-in-progress" since the protocol is under + development and the driver is not necessarily updated to the latest revision of it. +* Make prepared statement cache not be scoped by host and optimistically execute prepared statements on hosts where + we are not sure the statement is already prepared. The motivation is that in the steady state, all nodes have + prepared statements already, so there is no need to prepare statements before executing them. If the guess is wrong, + the client will prepare and execute at that point. +* Expose various cluster attributes with getters. + +Bug Fixes: +* [RUBY-235](https://datastax-oss.atlassian.net/browse/RUBY-235) execution_info.retries resets retry count when switching hosts. +* [RUBY-255](https://datastax-oss.atlassian.net/browse/RUBY-255) ControlConnection.peer_ip ignores peers that are missing critical information in system.peers. +* [RUBY-264](https://datastax-oss.atlassian.net/browse/RUBY-264) Table erroneously reported as using compact storage. + +# 3.0.3 + +Bug Fixes: +* [RUBY-241](https://datastax-oss.atlassian.net/browse/RUBY-241) Materialied views sometimes have nil ref to base-table. + +# 3.0.2 + +Bug Fixes: +* [RUBY-219](https://datastax-oss.atlassian.net/browse/RUBY-219) Sometimes get stack trace in metadata.rb due to failure in SortedSet initialization. +* [RUBY-220](https://datastax-oss.atlassian.net/browse/RUBY-220) Improve support for custom types. +* [RUBY-231](https://datastax-oss.atlassian.net/browse/RUBY-231) Driver ignores explicitly specified nil timeout (to indicate no time limit on query execution). +* [RUBY-233](https://datastax-oss.atlassian.net/browse/RUBY-233) Client timeout errors are retried for non-idempotent statements. + +# 3.0.0 GA +Features: +* Increased default request timeout (the `timeout` option to `Cassandra.cluster`), from 10 seconds to 12 seconds + because C* defaults to a 10 second timeout internally. The extra two seconds is buffer so that the client can + report the timeout in the server. This is also consistent with the Java driver. +* Expand :client_timestamps cluster configuration option to allow user to specify his own generator for client timestamps. + +Bug Fixes: +* [RUBY-207](https://datastax-oss.atlassian.net/browse/RUBY-207) Get NoMethodError when handling a write-timeout error using a downgrading consistency retry policy. +* [RUBY-214](https://datastax-oss.atlassian.net/browse/RUBY-214) Client timestamps in JRuby are not fine-grained enough, causing timestamp collisions and lost rows in C*. Breaking Changes: +* The Datacenter-aware load balancing policy (Cassandra::LoadBalancing::Policies::DCAwareRoundRobin) defaults to using + nodes in the local DC only. In prior releases, the policy would fall back to remote nodes after exhausting local nodes. + Specify a positive value (or nil for unlimited) for `max_remote_hosts_to_use` when initializing the policy to allow remote node use. + +# 3.0.0 rc2 +Features: +* Add protocol_version configuration option to allow the user to force the protocol version to use for communication with nodes. +* Expose listen_address and broadcast_address in `Cassandra::Host` if available. +* Add support for materialized views in the schema metadata. +* Add support for Cassandra indexes in the schema metadata. +* Add or expose the id, options, keyspace, partition_key, clustering_columns, and clustering_order attributes to table and view schema objects. +* Add crc_check_chance and extensions attributes to ColumnContainer options. +* Make cluster configuration options list publicly available. (Thanks, Evan Prothro!) + +Bug Fixes: +* [RUBY-161](https://datastax-oss.atlassian.net/browse/RUBY-161) Protocol version negotiation in mixed version clusters should not fall back to v1 unless it is truly warranted. +* [RUBY-180](https://datastax-oss.atlassian.net/browse/RUBY-180) Column ordering is not deterministic in Table metadata. +* [RUBY-185](https://datastax-oss.atlassian.net/browse/RUBY-185) Internal columns in static-compact and dense tables should be ignored. +* [RUBY-186](https://datastax-oss.atlassian.net/browse/RUBY-186) Custom type column metadata should be parsed properly for C* 3.x schemas. # 3.0.0 rc1 @@ -13,9 +84,9 @@ Features: * Add Cassandra::Logger class to make it easy for users to enable debug logging in the client. Bug Fixes: -* [RUBY-154] Improve batch request performance, which had regressed in 3.0.0 beta1. -* [RUBY-155] Request timeout timer should not include request queuing time. -* [RUBY-156] Do not drop response frames that follow a frame containing a warning. +* [RUBY-154](https://datastax-oss.atlassian.net/browse/RUBY-154) Improve batch request performance, which had regressed in 3.0.0 beta1. +* [RUBY-155](https://datastax-oss.atlassian.net/browse/RUBY-155) Request timeout timer should not include request queuing time. +* [RUBY-156](https://datastax-oss.atlassian.net/browse/RUBY-156) Do not drop response frames that follow a frame containing a warning. # 3.0.0 beta1 @@ -35,10 +106,10 @@ Features: Bug Fixes: -* [RUBY-143] Retry querying system table for metadata of new hosts when prior attempts fail, ultimately enabling use of new hosts. -* [RUBY-150] Fixed a protocol decoding error that occurred when multiple messages are available in a stream. -* [RUBY-151] Decode incomplete UDTs properly. -* [RUBY-120] Tuples and UDTs can be used in sets and hash keys. +* [RUBY-143](https://datastax-oss.atlassian.net/browse/RUBY-143) Retry querying system table for metadata of new hosts when prior attempts fail, ultimately enabling use of new hosts. +* [RUBY-150](https://datastax-oss.atlassian.net/browse/RUBY-150) Fixed a protocol decoding error that occurred when multiple messages are available in a stream. +* [RUBY-151](https://datastax-oss.atlassian.net/browse/RUBY-151) Decode incomplete UDTs properly. +* [RUBY-120](https://datastax-oss.atlassian.net/browse/RUBY-120) Tuples and UDTs can be used in sets and hash keys. Breaking Changes: @@ -48,6 +119,16 @@ Breaking Changes: * Unavailable errors are retried on the next host in the load balancing plan by default. * Statement execution no longer retried on timeouts, unless `:idempotent => true` has been specified when executing. +# 2.1.7 +Bug Fixes: +* [RUBY-255](https://datastax-oss.atlassian.net/browse/RUBY-255) ControlConnection.peer_ip ignores peers that are missing critical information in system.peers. + +# 2.1.6 +Bug Fixes: + +* [RUBY-202](https://datastax-oss.atlassian.net/browse/RUBY-202) Allow password authenticator to be used for LDAP authentication. This is actually a backport of + RUBY-169 for the 3.0.0 release. + # 2.1.5 Features: @@ -56,25 +137,25 @@ Features: Bug Fixes: -* [RUBY-128] Fix decoding of large values in maps, sets and lists. +* [RUBY-128](https://datastax-oss.atlassian.net/browse/RUBY-128) Fix decoding of large values in maps, sets and lists. # 2.1.4 Features: -* [RUBY-119] Use `require 'datastax/cassandra'` to avoid namespace conflicts -* [RUBY-90] Add support for disabling nagle algorithm (tcp nodelay), enabled by default. -* [RUBY-70] Add support for client-side timestamps, disabled by default. -* [RUBY-114] Add support for serial consistency in batch requests. +* [RUBY-90](https://datastax-oss.atlassian.net/browse/RUBY-90) Add support for disabling nagle algorithm (tcp nodelay), enabled by default. +* [RUBY-70](https://datastax-oss.atlassian.net/browse/RUBY-70) Add support for client-side timestamps, disabled by default. +* [RUBY-114](https://datastax-oss.atlassian.net/browse/RUBY-114) Add support for serial consistency in batch requests. Bug Fixes: -* [RUBY-103] Don't regenerate schema metadata for the same replication +* [RUBY-103](https://datastax-oss.atlassian.net/browse/RUBY-103) Don't regenerate schema metadata for the same replication strategies and options -* [RUBY-102] Allow custom types in schema metadata -* [RUBY-97] Allow disabling of the initial population of schema metadata -* [RUBY-95] Speed up generation of large token maps -* [RUBY-116] fix thread leak on connection error +* [RUBY-102](https://datastax-oss.atlassian.net/browse/RUBY-102) Allow custom types in schema metadata +* [RUBY-97](https://datastax-oss.atlassian.net/browse/RUBY-97) Allow disabling of the initial population of schema metadata +* [RUBY-95](https://datastax-oss.atlassian.net/browse/RUBY-95) Speed up generation of large token maps +* [RUBY-116](https://datastax-oss.atlassian.net/browse/RUBY-116) fix thread leak on connection error +* [RUBY-119](https://datastax-oss.atlassian.net/browse/RUBY-119) Use `require 'datastax/cassandra'` to avoid namespace conflicts Breaking Changes: @@ -93,7 +174,7 @@ Release removing accidental debug code from 2.1.1. Bug Fixes: -* [RUBY-98] Use of undefined class variable in `Table#create_partition_key` +* [RUBY-98](https://datastax-oss.atlassian.net/browse/RUBY-98) Use of undefined class variable in `Table#create_partition_key` # 2.1.0 @@ -111,13 +192,13 @@ Breaking Changes: Bug Fixes: -* [RUBY-93] Reconnection can overflow the stack +* [RUBY-93](https://datastax-oss.atlassian.net/browse/RUBY-93) Reconnection can overflow the stack # 2.0.1 Bug Fixes: -* [RUBY-87] Decoder corrupts incomplete response buffer +* [RUBY-87](https://datastax-oss.atlassian.net/browse/RUBY-87) Decoder corrupts incomplete response buffer # 2.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b4cdcb24..ac9ba1b26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,18 +5,30 @@ All code has bugs, but if you report them they can be squashed. The best bug reports include everything that is needed to reliably reproduce the bug. + +### Running the Test Suite + Try to write a test case and include it in your report (have a look at the -[regression test suite](spec/integration/regression_spec.rb) if you need inspiration). -Submit defect reports to our [Jira](https://datastax-oss.atlassian.net/projects/RUBY/issues). +[regression test suite](spec/regressions) if you need inspiration). + +1. Bundle with `bundle install` +1. Run the unit test suite with `rake rspec` + * Using this rake task will install necessary ruby extensions as a prerequisite + * For `bundle exec rspec` to be successful, run `bundle exec rake compile` once, beforehand If it's not possible to write a test case, for example because the bug only happens in -very particular circumstances, or is not deterministic, make sure you include as much -information as you can about the situation. The version of the ruby driver is an absolute -must, the version of Ruby and Cassandra are also very important. If there is a stack trace -from the error make sure to include that (unfortunately the asynchronous nature of the -ruby driver means that the stack traces are not always as revealing as they could be). +very particular circumstances, or is not deterministic, please still report the bug! + +### Opening a ticket + +Submit defect reports to our [Jira](https://datastax-oss.atlassian.net/projects/RUBY/issues). Include: + +* The `cassandra-driver` version (`bundle show cassandra-driver | sed 's/.*\///'`) +* The Ruby version (`ruby -v`) +* The Cassandra version (2nd line printed when running `cqlsh`) +* A stack trace from the error, if there is one -##Pull Requests +## Pull Requests If you're able to fix a bug yourself, you can [fork the repository](https://help.github.com/articles/fork-a-repo/) and submit a diff --git a/Gemfile b/Gemfile index 4f4177bb2..a803540a9 100644 --- a/Gemfile +++ b/Gemfile @@ -2,10 +2,10 @@ source 'https://rubygems.org/' gemspec -gem 'snappy', :group => [:development, :test] -gem 'lz4-ruby', :group => [:development, :test] -gem 'rake-compiler', :group => [:development, :test] -gem 'cliver', :group => [:development, :test] +gem 'snappy', group: [:development, :test] +gem 'lz4-ruby', group: [:development, :test] +gem 'rake-compiler', group: [:development, :test] +gem 'cliver', group: [:development, :test] group :development do platforms :mri_19 do diff --git a/Gemfile.lock b/Gemfile.lock index 77ecb61af..28bb80d78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - cassandra-driver (3.0.0.rc.1) + cassandra-driver (3.2.1.rc.1) ione (~> 1.2) GEM @@ -40,9 +40,9 @@ GEM ffi (1.9.10) ffi (1.9.10-java) gherkin (3.2.0) - ione (1.2.3) - json (1.8.3) - json (1.8.3-java) + ione (1.2.4) + json (1.8.6) + json (1.8.6-java) lz4-ruby (0.3.3) lz4-ruby (0.3.3-java) minitest (4.7.5) @@ -121,4 +121,4 @@ DEPENDENCIES yard BUNDLED WITH - 1.11.2 + 1.14.6 diff --git a/LICENSE b/LICENSE index c0868f0d3..6132ffb09 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2013-2016 DataStax, Inc. +Copyright 2013-2017 DataStax, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 25d0a4928..bb75c944c 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,34 @@ # Datastax Ruby Driver for Apache Cassandra -*If you're reading this on GitHub, please note that this is the readme for the development version and that some features described here might not yet have been released. You can [find the documentation for latest version through ruby driver docs](http://datastax.github.io/ruby-driver/) or via the release tags, [e.g. v1.0.0-beta.3](https://github.com/datastax/ruby-driver/tree/v1.0.0-beta.3).* +*If you're reading this on GitHub, please note that this is the readme for the development version and that some +features described here might not yet have been released. You can view the documentation for the latest released +version [here](http://docs.datastax.com/en/developer/ruby-driver/latest).* [![Build Status](https://travis-ci.org/datastax/ruby-driver.svg?branch=master)](https://travis-ci.org/datastax/ruby-driver) A Ruby client driver for Apache Cassandra. This driver works exclusively with the Cassandra Query Language version 3 (CQL3) and Cassandra's native protocol. +Use the [Ruby DSE driver](https://github.com/datastax/ruby-dse-driver.git) for +better compatibility and support for DataStax Enterprise. + - Code: https://github.com/datastax/ruby-driver -- Docs: http://datastax.github.io/ruby-driver/ +- Docs: http://docs.datastax.com/en/developer/ruby-driver - Jira: https://datastax-oss.atlassian.net/browse/RUBY - Mailing List: https://groups.google.com/a/lists.datastax.com/forum/#!forum/ruby-driver-user -- IRC: #datastax-drivers on [irc.freenode.net](http://freenode.net>) -- Twitter: Follow the latest news about DataStax Drivers - [@avalanche123](http://twitter.com/avalanche123), [@stamhankar999](http://twitter.com/stamhankar999), [@al3xandru](https://twitter.com/al3xandru) +- Slack: `#datastax-drivers` channel at https://academy.datastax.com/slack +- Twitter: Follow the latest news about DataStax Drivers - [@stamhankar999](http://twitter.com/stamhankar999), [@avalanche123](http://twitter.com/avalanche123), [@al3xandru](https://twitter.com/al3xandru) This driver is based on [the cql-rb gem](https://github.com/iconara/cql-rb) by [Theo Hultberg](https://github.com/iconara) and we added support for: -* [Asynchronous execution](http://datastax.github.io/ruby-driver/features/asynchronous_io/) -* One-off, [prepared](http://datastax.github.io/ruby-driver/features/basics/prepared_statements/) and [batch statements](http://datastax.github.io/ruby-driver/features/basics/batch_statements/) -* Automatic peer discovery and cluster metadata with [support for change notifications](http://datastax.github.io/ruby-driver/features/state_listeners/) -* Various [load-balancing](http://datastax.github.io/ruby-driver/features/load_balancing/), [retry](http://datastax.github.io/ruby-driver/features/retry_policies/) and [reconnection](http://datastax.github.io/ruby-driver/features/reconnection/) policies with [ability to write your own](http://datastax.github.io/ruby-driver/features/load_balancing/implementing_a_policy/) -* [SSL encryption](http://datastax.github.io/ruby-driver/features/security/ssl_encryption/) -* [Flexible and robust error handling](http://datastax.github.io/ruby-driver/features/error_handling/) -* [Per-request execution information and tracing](http://datastax.github.io/ruby-driver/features/debugging/) -* [Configurable address resolution](http://datastax.github.io/ruby-driver/features/address_resolution/) +* [Asynchronous execution](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/asynchronous_io/) +* One-off, [prepared](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/basics/prepared_statements/) and [batch statements](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/basics/batch_statements/) +* Automatic peer discovery and cluster metadata with [support for change notifications](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/state_listeners/) +* Various [load-balancing](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/load_balancing/), [retry](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/retry_policies/) and [reconnection](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/reconnection/) policies with [ability to write your own](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/load_balancing/implementing_a_policy/) +* [SSL encryption](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/security/ssl_encryption/) +* [Flexible and robust error handling](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/error_handling/) +* [Per-request execution information and tracing](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/debugging/) +* [Configurable address resolution](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/address_resolution/) [Check out the slides from Ruby Driver Explained](https://speakerdeck.com/avalanche123/ruby-driver-explained) for a detailed overview of the Ruby Driver architecture. @@ -31,14 +36,19 @@ This driver is based on [the cql-rb gem](https://github.com/iconara/cql-rb) by [ This driver works exclusively with the Cassandra Query Language v3 (CQL3) and Cassandra's native protocol. The current version works with: -* Apache Cassandra versions 1.2, 2.0, 2.1, and 3.x -* DataStax Enterprise 4.0, 4.5, 4.6, 4.7, and 4.8 -* Ruby (MRI) 2.2, 2.3 -* JRuby 1.7 +* Apache Cassandra versions 2.1, 2.2, and 3.x +* DataStax Enterprise 4.8 and above. However, the [Ruby DSE driver](https://github.com/datastax/ruby-dse-driver.git) provides more features and is recommended for use with DataStax Enterprise. +* Ruby (MRI) 2.2, 2.3, 2.4 +* JRuby 1.7, 9k + +__Note__: Rubinius is not supported. MRI 2.0, and 2.1 are not officially supported, but they should work. MRI 1.9.3 is deprecated +and may break in any release after 3.0. + +__Note__: Big-endian systems are not supported. + +## Feedback Requested -__Note__: JRuby 1.6 is not officially supported, although 1.6.8 should work. Similarly, -MRI 2.0 and 2.1 are not officially supported, but they should work. 1.9.3 is deprecated -and is likely to break in the release following 3.0. +*Help us focus our efforts!* [Provide your input](http://goo.gl/forms/pCs8PTpHLf) on the Ruby Driver Platform and Runtime Survey (we kept it short). ## Quick start @@ -51,10 +61,10 @@ cluster.each_host do |host| # automatically discovers all peers puts "Host #{host.ip}: id=#{host.id} datacenter=#{host.datacenter} rack=#{host.rack}" end -keyspace = 'system' +keyspace = 'system_schema' session = cluster.connect(keyspace) # create session, optionally scoped to a keyspace, to execute queries -future = session.execute_async('SELECT keyspace_name, table_name FROM system_schema.tables') # fully asynchronous api +future = session.execute_async('SELECT keyspace_name, table_name FROM tables') # fully asynchronous api future.on_success do |rows| rows.each do |row| puts "The keyspace #{row['keyspace_name']} has a table called #{row['table_name']}" @@ -67,9 +77,9 @@ __Note__: The host you specify is just a seed node, the driver will automaticall Read more: -* [`Cassandra.cluster` options](http://datastax.github.io/ruby-driver/api/#cluster-class_method) -* [`Session#execute_async` options](http://datastax.github.io/ruby-driver/api/session/#execute_async-instance_method) -* [Usage documentation](http://datastax.github.io/ruby-driver/features) +* [`Cassandra.cluster` options](http://docs.datastax.com/en/developer/ruby-driver/3.0/api/cassandra/#cluster-class_method) +* [`Session#execute_async` options](http://docs.datastax.com/en/developer/ruby-driver/3.0/api/cassandra/session/#execute_async-instance_method) +* [Usage documentation](http://docs.datastax.com/en/developer/ruby-driver/3.0/features) ## Installation @@ -85,37 +95,65 @@ Install via Gemfile gem 'cassandra-driver' ``` -__Note__: if you want to use compression you should also install [snappy](http://rubygems.org/gems/snappy) or [lz4-ruby](http://rubygems.org/gems/lz4-ruby). [Read more about compression.](http://datastax.github.io/ruby-driver/features/#compression) +__Note__: if you want to use compression you should also install [snappy](http://rubygems.org/gems/snappy) or [lz4-ruby](http://rubygems.org/gems/lz4-ruby). [Read more about compression.](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/#compression) ## Upgrading from cql-rb -Some of the new features added to the driver have unfortunately led to changes in the original cql-rb API. In the examples directory, you can find [an example of how to wrap the ruby driver to achieve almost complete interface parity with cql-rb](https://github.com/datastax/ruby-driver/blob/master/examples/cql-rb-wrapper.rb) to assist you with gradual upgrade. +Some of the new features added to the driver have unfortunately led to changes in the original cql-rb API. +In the examples directory, you can find [an example of how to wrap the ruby driver to achieve almost complete +interface parity with cql-rb](https://github.com/datastax/ruby-driver/blob/master/examples/cql-rb-wrapper.rb) +to assist you with gradual upgrade. + +If you are upgrading to DataStax Enterprise, use the [Ruby DSE driver](https://github.com/datastax/ruby-dse-driver.git) +for more features and better compatibility. + +## What's new in v3.2 +This minor release adds support for MRI 2.4.x and also contains a few miscellaneous defect fixes. + +See the [changelog](https://github.com/datastax/ruby-driver/blob/master/CHANGELOG.md) for more information on all +changes in this version and past versions. + +## What's new in v3.1 -## What's new in v3.0.0 +This minor release introduces features and fixes around resiliency, schema metadata, usability, and performance. One +of the most user-impacting of these is the introduction of +[execution profiles](http://docs.datastax.com/en/developer/ruby-driver/3.1/features/basics/execution_profiles). +Execution profiles allow you to group various execution options into a 'profile' and you reference the desired +profile at execution time. Get the scoop +[here](http://docs.datastax.com/en/developer/ruby-driver/3.1/features/basics/execution_profiles). + +## What's new in v3.0 ### Features: -* Apache Cassandra native protocol v4 -* Add support for smallint, tinyint, date (Cassandra::Date) and time (Cassandra::Time) data types. +* Add support for Apache Cassandra native protocol v4 +* Add support for smallint, tinyint, date (`Cassandra::Date`) and time (`Cassandra::Time`) data types. * Include schema metadata for User Defined Functions and User Defined Aggregates. +* Augment the `Cassandra::Table` object to expose many more attributes: `id`, `options`, `keyspace`, `partition_key`, `clustering_columns`, and `clustering_order`. This makes it significantly easier to write administration scripts that report various attributes of your schema, which may help to highlight areas for improvement. * Include client ip addresses in request traces, only on Cassandra 3.x. -* Add new retry policy decision Cassandra::Retry::Policy#try_next_host. -* Support specifying statement idempotence with the new :idempotent option when executing. -* Support sending custom payloads when preparing or executing statements using the new :payload option. -* Expose custom payloads received with responses on server exceptions and Cassandra::Execution::Info instances. -* Expose server warnings on server exceptions and Cassandra::Execution::Info instances. -* Add connections_per_local_node, connections_per_remote_node, requests_per_connection cluster configuration options to tune parallel query execution and resource usage. -* Add Cassandra::Logger class to make it easy for users to enable debug logging in the client. - -### Breaking Changes: - -* Cassandra::Future#join is now an alias to Cassandra::Future#get and will raise an error if the future is resolved with one. +* Add new retry policy decision `Cassandra::Retry::Policy#try_next_host`. +* Support specifying statement idempotence with the new `idempotent` option when executing. +* Support sending custom payloads when preparing or executing statements using the new `payload` option. +* Expose custom payloads received with responses on server exceptions and `Cassandra::Execution::Info` instances. +* Expose server warnings on server exceptions and `Cassandra::Execution::Info` instances. +* Add `connections_per_local_node`, `connections_per_remote_node`, `requests_per_connection` cluster configuration options to tune parallel query execution and resource usage. +* Add `Cassandra::Logger` class to make it easy for users to enable debug logging in the client. +* Add `protocol_version` configuration option to allow the user to force the protocol version to use for communication with nodes. +* Add support for materialized views and indexes in the schema metadata. +* Support the `ReadError`, `WriteError`, and `FunctionCallError` Cassandra error responses introduced in Cassandra 2.2. +* Add support for unset variables in bound statements. +* Support DSE security (`DseAuthenticator`, configured for LDAP). +* Add a timeout option to `Cassandra::Future#get`. + +### Breaking Changes from 2.x: + +* `Cassandra::Future#join` is now an alias to Cassandra::Future#get and will raise an error if the future is resolved with one. * Default consistency level is now LOCAL_ONE. * Enable tcp no-delay by default. * Unavailable errors are retried on the next host in the load balancing plan by default. -* Statement execution no longer retried on timeouts, unless :idempotent => true has been specified when executing. -* Cassandra::Statements::Batch#add signature has changed in how one specifies query parameters. Specify the query parameters array as the value of the arguments key: +* Statement execution no longer retried on timeouts, unless the statement is marked as idempotent in the call to `Cassandra::Session#execute*` or when creating a `Cassandra::Statement` object. +* `Cassandra::Statements::Batch#add` and `Cassandra::Session#execute*` signatures have changed in how one specifies query parameters. Specify the query parameters array as the value of the arguments key: ```ruby batch.add(query, ['val1', 'val2']) @@ -126,18 +164,10 @@ batch.add(query, {p1: 'val1'}) # becomes batch.add(query, arguments: {p1: 'val1'}) ``` - -### Bug Fixes: - -* [[RUBY-120](https://datastax-oss.atlassian.net/browse/RUBY-120)] Tuples and UDTs can be used in sets and hash keys. -* [[RUBY-143](https://datastax-oss.atlassian.net/browse/RUBY-143)] Retry querying system table for metadata of new hosts when prior attempts fail, ultimately enabling use of new hosts. -* [[RUBY-150](https://datastax-oss.atlassian.net/browse/RUBY-150)] Fixed a protocol decoding error that occurred when multiple messages are available in a stream. -* [[RUBY-151](https://datastax-oss.atlassian.net/browse/RUBY-151)] Decode incomplete UDTs properly. - -## Feedback Requested - -*Help us focus our efforts!* [Provide your input](http://goo.gl/forms/pCs8PTpHLf) -on the Ruby Driver Platform and Runtime Survey (we kept it short). +* The Datacenter-aware load balancing policy (`Cassandra::LoadBalancing::Policies::DCAwareRoundRobin`) defaults to using + nodes in the local DC only. In prior releases, the policy would fall back to remote nodes after exhausting local nodes. + Specify a positive value (or nil for unlimited) for `max_remote_hosts_to_use` when initializing the policy to allow remote node use. +* Unspecified variables in statements previously resulted in an exception. Now they are essentially ignored or treated as null. ## Code examples @@ -167,7 +197,7 @@ bundle install --without docs CASSANDRA_VERSION=3.1.1 bundle exec cucumber # runs end-to-end tests (or bundle exec rake cucumber) CASSANDRA_VERSION=3.0.0 bundle exec rspec # runs unit tests (or bundle exec rake rspec) CASSANDRA_VERSION=2.1.12 bundle exec rake integration # run integration tests -CASSANDRA_VERSION=2.0.17 bundle exec rake test # run both as well as integration tests +CASSANDRA_VERSION=2.1.12 bundle exec rake test # run both as well as integration tests ``` ## Changelog & versioning @@ -191,11 +221,12 @@ the release. ## Known bugs & limitations -* JRuby 1.6 is not officially supported, although 1.6.8 should work. +* Specifying a `protocol_version` option of 1 or 2 in cluster options will fail with a + `NoHostsAvailable` error rather than a `ProtocolError` against Cassandra node versions 3.0-3.4. * Because the driver reactor is using `IO.select`, the maximum number of tcp connections allowed is 1024. * Because the driver uses `IO#write_nonblock`, Windows is not supported. -Please [refer to the usage documentation for more information on common pitfalls](http://datastax.github.io/ruby-driver/features/) +Please [refer to the usage documentation for more information on common pitfalls](http://docs.datastax.com/en/developer/ruby-driver/3.0/features/) ## Contributing @@ -209,11 +240,11 @@ that are common across all other DataStax drivers for Apache Cassandra. The development effort to provide an up to date, high performance, fully featured Ruby Driver for Apache Cassandra will continue on this project, while -[cql-rb](https://github.com/iconara/cql-rb/) will be discontinued. +[cql-rb](https://github.com/iconara/cql-rb/) has been discontinued. ## Copyright -Copyright 2013-2016 DataStax, Inc. +Copyright 2013-2017 DataStax, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -225,4 +256,4 @@ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF either express or implied. See the License for the specific language governing permissions and limitations under the License. - [1]: http://datastax.github.io/ruby-driver/api + [1]: http://docs.datastax.com/en/developer/ruby-driver/3.0/api diff --git a/Rakefile b/Rakefile index 6966aa898..da09c02a0 100644 --- a/Rakefile +++ b/Rakefile @@ -7,16 +7,28 @@ require 'cucumber/rake/task' require 'rake/testtask' require 'bundler/gem_tasks' -ENV["FAIL_FAST"] ||= 'Y' +ENV['FAIL_FAST'] ||= 'Y' -RSpec::Core::RakeTask.new(:rspec => :compile) +RSpec::Core::RakeTask.new(rspec: :compile) -Cucumber::Rake::Task.new(:cucumber => :compile) +# We separate interactive from non-interactive features because jruby 9k sometimes has trouble +# closing its pipe to the child process in interactive features. + +Cucumber::Rake::Task.new({cucumber_interactive: :compile}, 'Run cucumber features that are interactive') do |t| + t.profile = 'interactive' +end + +Cucumber::Rake::Task.new({cucumber_noninteractive: :compile}, 'Run cucumber features that are non-interactive') do |t| + t.profile = 'non_interactive' +end + +desc 'Run cucumber features' +task cucumber: [:cucumber_noninteractive, :cucumber_interactive] desc 'Run all tests' -task :test => [:rspec, :integration, :cucumber] +task test: [:rspec, :integration, :cucumber] -ruby_engine = defined?(RUBY_ENGINE)? RUBY_ENGINE : 'ruby' +ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby' case ruby_engine when 'jruby' @@ -29,18 +41,19 @@ else Rake::ExtensionTask.new('cassandra_murmur3') end -Rake::TestTask.new(:integration => :compile) do |t| - t.libs.push "lib" +Rake::TestTask.new(integration: :compile) do |t| + t.libs.push 'lib' t.test_files = FileList['integration/*_test.rb', - 'integration/security/*_test.rb', - 'integration/load_balancing/*_test.rb', - 'integration/types/*_test.rb', - 'integration/functions/*_test.rb'] + 'integration/security/*_test.rb', + 'integration/load_balancing/*_test.rb', + 'integration/types/*_test.rb', + 'integration/functions/*_test.rb', + 'integration/indexes/*_test.rb'] t.verbose = true end -Rake::TestTask.new(:stress => :compile) do |t| - t.libs.push "lib" +Rake::TestTask.new(stress: :compile) do |t| + t.libs.push 'lib' t.test_files = FileList['integration/stress_tests/*_test.rb'] t.verbose = true end diff --git a/benchmarking/async-prepared-insert-cql-rb.rb b/benchmarking/async-prepared-insert-cql-rb.rb index d6344c20f..9c2f1f014 100644 --- a/benchmarking/async-prepared-insert-cql-rb.rb +++ b/benchmarking/async-prepared-insert-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-prepared-insert-ruby-driver.rb b/benchmarking/async-prepared-insert-ruby-driver.rb index d874dadd1..90d73eea3 100644 --- a/benchmarking/async-prepared-insert-ruby-driver.rb +++ b/benchmarking/async-prepared-insert-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-prepared-select-cql-rb.rb b/benchmarking/async-prepared-select-cql-rb.rb index 815dfbfa8..01ce04f11 100644 --- a/benchmarking/async-prepared-select-cql-rb.rb +++ b/benchmarking/async-prepared-select-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-prepared-select-ruby-driver.rb b/benchmarking/async-prepared-select-ruby-driver.rb index 7211f5ba9..4d267115b 100644 --- a/benchmarking/async-prepared-select-ruby-driver.rb +++ b/benchmarking/async-prepared-select-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-unprepared-insert-cql-rb.rb b/benchmarking/async-unprepared-insert-cql-rb.rb index a0dcf20f2..fa5aea5a5 100644 --- a/benchmarking/async-unprepared-insert-cql-rb.rb +++ b/benchmarking/async-unprepared-insert-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-unprepared-insert-ruby-driver.rb b/benchmarking/async-unprepared-insert-ruby-driver.rb index 1410920e1..420505bf3 100644 --- a/benchmarking/async-unprepared-insert-ruby-driver.rb +++ b/benchmarking/async-unprepared-insert-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-unprepared-select-cql-rb.rb b/benchmarking/async-unprepared-select-cql-rb.rb index 0b53b5f82..fb55c48fc 100644 --- a/benchmarking/async-unprepared-select-cql-rb.rb +++ b/benchmarking/async-unprepared-select-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/async-unprepared-select-ruby-driver.rb b/benchmarking/async-unprepared-select-ruby-driver.rb index b8d66a183..3eb3ebb3f 100644 --- a/benchmarking/async-unprepared-select-ruby-driver.rb +++ b/benchmarking/async-unprepared-select-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/benchmark.rb b/benchmarking/benchmark.rb index 0199f06b2..b0dba8b4f 100644 --- a/benchmarking/benchmark.rb +++ b/benchmarking/benchmark.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-prepared-insert-cql-rb.rb b/benchmarking/sync-prepared-insert-cql-rb.rb index 6fe78f4d2..fd8f4ba3a 100644 --- a/benchmarking/sync-prepared-insert-cql-rb.rb +++ b/benchmarking/sync-prepared-insert-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-prepared-insert-ruby-driver.rb b/benchmarking/sync-prepared-insert-ruby-driver.rb index f8be56018..23fecd7d3 100644 --- a/benchmarking/sync-prepared-insert-ruby-driver.rb +++ b/benchmarking/sync-prepared-insert-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-prepared-select-cql-rb.rb b/benchmarking/sync-prepared-select-cql-rb.rb index cbeecc590..d342d0101 100644 --- a/benchmarking/sync-prepared-select-cql-rb.rb +++ b/benchmarking/sync-prepared-select-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-prepared-select-ruby-driver.rb b/benchmarking/sync-prepared-select-ruby-driver.rb index f0e85f7a5..398b7e8cf 100644 --- a/benchmarking/sync-prepared-select-ruby-driver.rb +++ b/benchmarking/sync-prepared-select-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-unprepared-insert-cql-rb.rb b/benchmarking/sync-unprepared-insert-cql-rb.rb index 7daf80360..610637b5f 100644 --- a/benchmarking/sync-unprepared-insert-cql-rb.rb +++ b/benchmarking/sync-unprepared-insert-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-unprepared-insert-ruby-driver.rb b/benchmarking/sync-unprepared-insert-ruby-driver.rb index b78370efb..9035f6ec3 100644 --- a/benchmarking/sync-unprepared-insert-ruby-driver.rb +++ b/benchmarking/sync-unprepared-insert-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-unprepared-select-cql-rb.rb b/benchmarking/sync-unprepared-select-cql-rb.rb index 7228d87ee..9255d899d 100644 --- a/benchmarking/sync-unprepared-select-cql-rb.rb +++ b/benchmarking/sync-unprepared-select-cql-rb.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/benchmarking/sync-unprepared-select-ruby-driver.rb b/benchmarking/sync-unprepared-select-ruby-driver.rb index 8145318a0..5908da7bd 100644 --- a/benchmarking/sync-unprepared-select-ruby-driver.rb +++ b/benchmarking/sync-unprepared-select-ruby-driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/bin/console b/bin/console new file mode 100755 index 000000000..e10ffd76a --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "cassandra" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +begin + require "pry" + Pry.start +rescue LoadError + require "irb" + IRB.start +end diff --git a/build.yaml b/build.yaml index 269713fd7..af62ccc29 100644 --- a/build.yaml +++ b/build.yaml @@ -1,13 +1,26 @@ +schedules: + commit: + schedule: per_commit + matrix: + exclude: + - ruby: ['2.2', 'jruby1.7'] + - cassandra: ['2.2', '3.0'] + nightly: + schedule: nightly + branches: + include: [master] + ruby: - 2.2 - 2.3 + - 2.4 - jruby1.7 + - jruby9k cassandra: - - 2.0 - 2.1 - 2.2 - 3.0 - - 3.3 + - '3.10' os: - ubuntu/trusty64 build: diff --git a/cassandra-driver.gemspec b/cassandra-driver.gemspec index e5f84a8be..45002f1e9 100644 --- a/cassandra-driver.gemspec +++ b/cassandra-driver.gemspec @@ -1,6 +1,6 @@ # encoding: utf-8 -$: << File.expand_path('../lib', __FILE__) +$LOAD_PATH << File.expand_path('../lib', __FILE__) require 'cassandra/version' @@ -10,8 +10,8 @@ Gem::Specification.new do |s| s.authors = ['Theo Hultberg', 'Bulat Shakirzyanov', 'Sandeep Tamhankar'] s.email = ['theo@iconara.net', 'bulat.shakirzyanov@datastax.com', 'sandeep.tamhankar@datastax.com'] s.homepage = 'http://datastax.github.io/ruby-driver' - s.summary = %q{Datastax Ruby Driver for Apache Cassandra} - s.description = %q{A pure Ruby driver for Apache Cassandra} + s.summary = 'Datastax Ruby Driver for Apache Cassandra' + s.description = 'A pure Ruby driver for Apache Cassandra' s.license = 'Apache License 2.0' s.files = Dir['lib/**/*.rb', 'README.md', '.yardopts'] s.require_paths = %w(lib) diff --git a/cucumber.yml b/cucumber.yml index 83a98e36a..e4818fa0a 100644 --- a/cucumber.yml +++ b/cucumber.yml @@ -36,3 +36,5 @@ if cassandra_version.start_with?('1.2') end %> default: --color --guess --tags ~@todo --tags ~@cassandra-version-specific<%= cassandra_version_tags %> -r features/ +non_interactive: --color --guess --tags ~@interactive --tags ~@todo --tags ~@cassandra-version-specific<%= cassandra_version_tags %> -r features/ +interactive: --color --guess --tags @interactive --tags ~@todo --tags ~@cassandra-version-specific<%= cassandra_version_tags %> -r features/ diff --git a/docs.yaml b/docs.yaml index 9c42eb5ca..29abb7c00 100644 --- a/docs.yaml +++ b/docs.yaml @@ -1,6 +1,6 @@ title: Ruby Driver for Apache Cassandra summary: A pure Ruby client for Apache Cassandra -homepage: http://datastax.github.io/ruby-driver/ +homepage: http://docs.datastax.com/en/developer/ruby-driver/ sections: - title: Features prefix: /features @@ -18,16 +18,20 @@ sections: rewrites: - api/: api/cassandra/ - cassandra/cassandra: cassandra + - supplemental/: / + - en/latest-ruby-driver/ruby-driver/whatsNew.html: en/developer/ruby-driver/latest - 'trunk/doc/native_protocol_v([12])\.spec': cassandra-2.2/doc/native_protocol_v\1.spec - search: http://www.datastax.com/documentation/cql/3.1/webhelp/index.html replace: http://docs.datastax.com/en/cql/3.3/cql/cql_using/useAboutCQL.html - search: https://datastax.github.io/java-driver/features replace: https://datastax.github.io/java-driver/manual + - search: http://dsdocs30_java + replace: http://docs.datastax.com/en/developer/java-driver/3.0 links: - title: Code href: https://github.com/datastax/ruby-driver/ - title: Docs - href: http://datastax.github.io/ruby-driver/ + href: http://docs.datastax.com/en/developer/ruby-driver/ - title: Issues href: https://datastax-oss.atlassian.net/browse/RUBY/ - title: Mailing List @@ -37,17 +41,19 @@ links: - title: Releases href: https://github.com/datastax/ruby-driver/releases versions: - - name: v3.0.0.rc.1 - ref: c8ed32ce85b608f172e72133dd68c3e2af34db00 - - name: v3.0.0.beta.1 - ref: a0a5f7a5cc308497e5e865f008130441722208e5 - - name: v2.1.5 - ref: bbb7084d11be701ff42f298373c0255b9a3e2b7f - - name: v2.0.1 + - name: 3.2 + ref: v3.2.0 + - name: 3.1 + ref: v3.1.0 + - name: 3.0 + ref: v3.0.3 + - name: 2.1 + ref: v2.1.7 + - name: 2.0 ref: 80cab0ad188511ec16eb39111c0b67329c754729 - - name: v1.2.0 + - name: 1.2 ref: 6c398cb3dbc5218f6a77177dcc2130d11bf0158a - - name: v1.1.1 + - name: 1.1 ref: 1763066e2f70db8889799aa0af4a8eb63ad9ab74 - - name: v1.0.0 + - name: 1.0 ref: 72bfc9609f94e904c0186b061a02404fb2c0d22e diff --git a/ext/cassandra_murmur3/CassandraMurmur3Service.java b/ext/cassandra_murmur3/CassandraMurmur3Service.java index 8ce598703..aa3576e48 100644 --- a/ext/cassandra_murmur3/CassandraMurmur3Service.java +++ b/ext/cassandra_murmur3/CassandraMurmur3Service.java @@ -1,4 +1,4 @@ -// Copyright 2013-2016 DataStax, Inc. +// Copyright 2013-2017 DataStax, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ext/cassandra_murmur3/cassandra_murmur3.c b/ext/cassandra_murmur3/cassandra_murmur3.c index 26241e4a2..04ecabda1 100644 --- a/ext/cassandra_murmur3/cassandra_murmur3.c +++ b/ext/cassandra_murmur3/cassandra_murmur3.c @@ -1,4 +1,4 @@ -// Copyright 2013-2016 DataStax, Inc. +// Copyright 2013-2017 DataStax, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/features/README.md b/features/README.md index e190401cb..b5d4713c9 100644 --- a/features/README.md +++ b/features/README.md @@ -80,7 +80,7 @@ statement = session.prepare('INSERT INTO users (username, email) VALUES (?, ?)') session.execute(statement, arguments: ['avalanche123', 'bulat.shakirzyanov@datastax.com']) ``` -You should prepare a statement for a given query **only** once and then resue it by calling #execute. Re-preparing the same statement will have a negative impact on the performance and should be avoided. +You should prepare a statement for a given query **only** once and then reuse it by calling #execute. Re-preparing the same statement will have a negative impact on the performance and should be avoided. A prepared statement can be run many times, but the CQL parsing will only be done once on each node. Use prepared statements for queries you run over and over again. diff --git a/features/basics/client_side_timestamps.feature b/features/basics/client_side_timestamps.feature index 70a66a1f4..4b72cf83f 100644 --- a/features/basics/client_side_timestamps.feature +++ b/features/basics/client_side_timestamps.feature @@ -23,7 +23,15 @@ Feature: Client-side Timestamps require 'cassandra' require 'delorean' - cluster = Cassandra.cluster(client_timestamps: true) + # Enable client-side timestamps. Valid values: + # nil / false : don't enable client-side timestamps. This is the default. + # true : enable client-side timestamps and use the default timestamp generator for the Ruby flavor being used: + # JRuby - Cassandra::TimestampGenerator::TickingOnDuplicate + # MRI/Rubinius - Cassandra::TimestampGenerator::Simple + # :simple : enable client-side timestamps and use a Cassandra::TimestampGenerator::Simple generator. + # :monotonic : enable client-side timestamps and use a Cassandra::TimestampGenerator::TickingOnDuplicate generator. + + cluster = Cassandra.cluster(client_timestamps: :simple) session = cluster.connect("simplex") # Insert in the present diff --git a/features/basics/datatypes.feature b/features/basics/datatypes.feature index 94d6b46c4..1c1fbeb76 100644 --- a/features/basics/datatypes.feature +++ b/features/basics/datatypes.feature @@ -88,7 +88,11 @@ Feature: Datatypes row = session.execute("SELECT * FROM mytable").first puts "Bigint: #{row['b']}" - puts "Decimal: #{row['c']}" + + # Different versions of ruby express scientific notation with lowercase or capital E (E10 vs e10). + # Unify on upper-case + puts "Decimal: #{row['c'].to_s.upcase}" + puts "Double: #{row['d']}" puts "Float: #{row['e']}" puts "Integer: #{row['f']}" diff --git a/features/basics/execution_profiles.feature b/features/basics/execution_profiles.feature new file mode 100644 index 000000000..e8fadb416 --- /dev/null +++ b/features/basics/execution_profiles.feature @@ -0,0 +1,145 @@ +Feature: Execution profiles + + Execution profiles allow a user to group various execution options into a 'profile'. A user defines profiles when + initializing the cluster object. A user can then execute statements with different profiles by specifying the profile + name in calls to `Session.execute*`. + + Profile names should be strings or symbols. In this release, a profile encapsulates load-balancing policy, + retry-policy, consistency-level, and timeout primitive options. Execution profiles are immutable once created. + + If a user specifies simple options to `Cassandra.cluster`, the options mentioned above are stored in a default + execution profile named `:default`. This execution profile is used by default by `Session.execute*` methods. + User-defined execution profiles fall back to the same system defaults that past versions of the driver fell back to + for unspecified options: + + * load_balancing_policy: `LoadBalancing::Policies::TokenAware.new(LoadBalancing::Policies::DCAwareRoundRobin.new, true)` + * retry-policy: `Retry::Policies::Default.new` + * consistency: `:local_one` + * timeout: `12` + + In particular, note that user-defined execution profiles do not fall back to options specified in a (possibly + user-defined) `:default` profile. + + If you declare execution profiles, it is illegal to also include the primitive options mentioned above: + + ```ruby + puts "This is bad" + cluster = Cassandra.cluster(timeout: 7, execution_profiles: { + my_profile: Cassandra::Execution::Profile.new(...) + }) + ``` + + To change default execution profile attributes and also declare other execution profiles, you must explicitly declare + the :default profile when initializing the cluster: + + ```ruby + puts "This is legal" + cluster = Cassandra.cluster(execution_profiles: { + default: Cassandra::Execution::Profile.new(timeout: 7) + my_profile: Cassandra::Execution::Profile.new(...) + }) + ``` + + Unspecified attributes fall back to the system defaults mentioned above. + + Finally, options specified to `Session.execute*` methods override options specified in the desired execution profile. + + Background: + Given a running cassandra cluster + + Scenario: Creating and inspecting execution profiles + Given the following example: + """ruby + require 'cassandra' + + profile_1 = Cassandra::Execution::Profile.new(load_balancing_policy: Cassandra::LoadBalancing::Policies::RoundRobin.new, + retry_policy: Cassandra::Retry::Policies::DowngradingConsistency.new, + consistency: :all, + timeout: 32 + ) + + cluster = Cassandra.cluster(execution_profiles: {'my_profile' => profile_1}) + puts "There are #{cluster.execution_profiles.size} execution profiles in the cluster:" + puts "" + + cluster.execution_profiles.each do |name, profile| + puts "Name: #{name}" + puts "Load_balancing_policy: #{profile.load_balancing_policy.class}" + puts "Retry policy: #{profile.retry_policy.class}" + puts "Consistency: #{profile.consistency}" + puts "Timeout: #{profile.timeout}" + puts "" + end + """ + When it is executed + Then its output should contain: + """ + There are 2 execution profiles in the cluster: + + Name: my_profile + Load_balancing_policy: Cassandra::LoadBalancing::Policies::RoundRobin + Retry policy: Cassandra::Retry::Policies::DowngradingConsistency + Consistency: all + Timeout: 32 + + Name: default + Load_balancing_policy: Cassandra::LoadBalancing::Policies::TokenAware + Retry policy: Cassandra::Retry::Policies::Default + Consistency: local_one + Timeout: 12 + """ + + Scenario: Configure different load balancing policies with profiles + Given the following example: + """ruby + require 'cassandra' + + include Cassandra::LoadBalancing::Policies + profiles = { + p1: Cassandra::Execution::Profile.new(load_balancing_policy: WhiteList.new(['127.0.0.1'], RoundRobin.new)), + p2: Cassandra::Execution::Profile.new(load_balancing_policy: WhiteList.new(['127.0.0.2'], RoundRobin.new)) + } + + cluster = Cassandra.cluster(execution_profiles: profiles) + session = cluster.connect + + puts "Running with default profile" + + # By default, the driver uses a token-aware, round-robin load balancing policy. + ip_list = [] + 3.times do + rs = session.execute('select rpc_address from system.local') + ip_list << rs.first['rpc_address'].to_s + end + puts ip_list.sort.join("\n") + + # p2 and p3 set up load-balancing policies that will match only one node. + puts "Running with profile p1" + 3.times do + rs = session.execute('select rpc_address from system.local', execution_profile: :p1) + puts rs.first['rpc_address'] + end + + puts "Running with profile p2" + 3.times do + rs = session.execute('select rpc_address from system.local', execution_profile: :p2) + puts rs.first['rpc_address'] + end + """ + When it is executed + Then its output should contain: + """ + Running with default profile + 127.0.0.1 + 127.0.0.2 + 127.0.0.3 + Running with profile p1 + 127.0.0.1 + 127.0.0.1 + 127.0.0.1 + Running with profile p2 + 127.0.0.2 + 127.0.0.2 + 127.0.0.2 + """ + diff --git a/features/basics/prepared_statements.feature b/features/basics/prepared_statements.feature index 0c0c0263c..7dd4145a1 100644 --- a/features/basics/prepared_statements.feature +++ b/features/basics/prepared_statements.feature @@ -4,7 +4,12 @@ Feature: Prepared statements it multiple times with different values. A bind variable marker `?` is used to represent a dynamic value in a statement. - Scenario: an INSERT statement is prepared + The driver caches prepared statement id's in such a way that + when a query is to be prepared for a node, the "preparation" is bypassed if the statement has previously + been prepared on any other node. This reduces a round-trip to the node while also optimizing query execution + performance. + + Scenario: An INSERT statement is prepared Given a running cassandra cluster with schema: """cql CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; @@ -72,7 +77,7 @@ Feature: Prepared statements """ @cassandra-version-specific @cassandra-version-2.1 - Scenario: an INSERT statement is prepared with named parameters + Scenario: An INSERT statement is prepared with named parameters Given a running cassandra cluster with schema: """cql CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; @@ -140,7 +145,7 @@ Feature: Prepared statements """ @cassandra-version-specific @cassandra-version-2.0 - Scenario: a SELECT statement with parameterized LIMIT is prepared + Scenario: A SELECT statement with parameterized LIMIT is prepared Given a running cassandra cluster with schema: """cql CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; @@ -284,4 +289,4 @@ Feature: Prepared statements Then its output should contain: """ Joséphine Baker: La Petite Tonkinoise / Bye Bye Blackbird - """ \ No newline at end of file + """ diff --git a/features/basics/schema_metadata.feature b/features/basics/schema_metadata.feature index 3112a7234..3f21124e7 100644 --- a/features/basics/schema_metadata.feature +++ b/features/basics/schema_metadata.feature @@ -5,45 +5,223 @@ Feature: Schema Metadata Background: Given a running cassandra cluster - @cassandra-version-specific @cassandra-version-less-3.0 + Scenario: Getting keyspace metadata + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + """ + And the following example: + """ruby + require 'cassandra' + + cluster = Cassandra.cluster + + ks_meta = cluster.keyspace('simplex') + puts "Name: #{ks_meta.name}" + puts "Replication class: #{ks_meta.replication.klass}" + puts "Replication factor: #{ks_meta.replication.options['replication_factor'].to_i}" + puts "Durable writes?: #{ks_meta.durable_writes?}" + puts "# tables: #{ks_meta.tables.size}" + + puts "" + puts ks_meta.to_cql + """ + When it is executed + Then its output should contain: + """cql + Name: simplex + Replication class: SimpleStrategy + Replication factor: 3 + Durable writes?: true + # tables: 0 + + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'} AND durable_writes = true; + """ + Scenario: Getting table metadata - Given the following example: + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + CREATE TABLE simplex.users (user_id bigint, first text, last text, age int, PRIMARY KEY (user_id, last)) + """ + And the following example: """ruby require 'cassandra' cluster = Cassandra.cluster - puts cluster.keyspace('system').table("IndexInfo").to_cql + table_meta = cluster.keyspace('simplex').table('users') + puts "Name: #{table_meta.name}" + puts "Keyspace: #{table_meta.keyspace.name}" + puts "Partition key: #{table_meta.partition_key[0].name}" + puts "Clustering column: #{table_meta.clustering_columns[0].name}" + puts "Clustering order: #{table_meta.clustering_order.first}" + puts "Num columns: #{table_meta.columns.size}" + + puts "" + puts table_meta.to_cql """ When it is executed Then its output should contain: """cql - CREATE TABLE system."IndexInfo" ( - table_name text, - index_name text, - PRIMARY KEY (table_name, index_name) + Name: users + Keyspace: simplex + Partition key: user_id + Clustering column: last + Clustering order: asc + Num columns: 4 + + CREATE TABLE simplex."users" ( + user_id bigint, + last text, + age int, + first text, + PRIMARY KEY (user_id, last) ) """ + Scenario: Getting trigger metadata + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + CREATE TABLE simplex.test_table (a text primary key, b text); + CREATE TABLE simplex.audit (key timeuuid, keyspace_name text, table_name text, primary_key text, PRIMARY KEY(key)); + CREATE TRIGGER trigger1 ON simplex.test_table USING 'org.apache.cassandra.triggers.AuditTrigger'; + """ + And the following example: + """ruby + require 'cassandra' + + cluster = Cassandra.cluster + + # AuditTrigger is available in cassandra/conf/triggers. See cassandra/examples/triggers in Cassandra + trigger = cluster.keyspace('simplex').table('test_table').trigger('trigger1') + puts trigger.to_cql + + puts "" + puts "Name: #{trigger.name}" + puts "Table name: #{trigger.table.name}" + puts "Class: #{trigger.options['class']}" + """ + When it is executed + Then its output should contain: + """cql + CREATE TRIGGER "trigger1" ON simplex.test_table USING 'org.apache.cassandra.triggers.AuditTrigger'; + + Name: trigger1 + Table name: test_table + Class: org.apache.cassandra.triggers.AuditTrigger + """ + + Scenario: Getting index metadata + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + CREATE TABLE simplex.test_table (a int primary key, b int); + CREATE INDEX ind1 ON simplex.test_table (b); + """ + And the following example: + """ruby + require 'cassandra' + + cluster = Cassandra.cluster + + index = cluster.keyspace('simplex').table('test_table').index('ind1') + puts index.to_cql + + puts "" + puts "Name: #{index.name}" + puts "Table name: #{index.table.name}" + puts "Kind: #{index.kind}" + puts "Target: #{index.target}" + """ + When it is executed + Then its output should contain: + """cql + CREATE INDEX "ind1" ON simplex.test_table (b); + + Name: ind1 + Table name: test_table + Kind: composites + Target: b + """ + @cassandra-version-specific @cassandra-version-3.0 - Scenario: Getting table metadata on 3.0 - Given the following example: + Scenario: Getting index metadata on full collections + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + CREATE TABLE simplex.test_table (a int PRIMARY KEY, b frozen>); + CREATE INDEX ind1 ON simplex.test_table (full(b)); + """ + And the following example: """ruby require 'cassandra' cluster = Cassandra.cluster - puts cluster.keyspace('system').table("IndexInfo").to_cql + index = cluster.keyspace('simplex').table('test_table').index('ind1') + puts index.to_cql + + puts "" + puts "Name: #{index.name}" + puts "Table name: #{index.table.name}" + puts "Kind: #{index.kind}" + puts "Target: #{index.target}" """ When it is executed Then its output should contain: """cql - CREATE TABLE system."IndexInfo" ( - table_name text, - index_name text, - value 'org.apache.cassandra.db.marshal.EmptyType', - PRIMARY KEY (table_name, index_name) - ) + CREATE INDEX "ind1" ON simplex.test_table (full(b)); + + Name: ind1 + Table name: test_table + Kind: composites + Target: full(b) + """ + + @cassandra-version-specific @cassandra-version-3.0 + Scenario: Getting multiple index metadata on same column + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + CREATE TABLE simplex.test_table (a int PRIMARY KEY, b map); + CREATE INDEX ind1 ON simplex.test_table (keys(b)); + CREATE INDEX ind2 ON simplex.test_table (values(b)); + """ + And the following example: + """ruby + require 'cassandra' + + cluster = Cassandra.cluster + + index = cluster.keyspace('simplex').table('test_table').index('ind1') + puts index.to_cql + + puts "" + puts "Name: #{index.name}" + puts "Target: #{index.target}" + + puts "" + index = cluster.keyspace('simplex').table('test_table').index('ind2') + puts index.to_cql + + puts "" + puts "Name: #{index.name}" + puts "Target: #{index.target}" + """ + When it is executed + Then its output should contain: + """cql + CREATE INDEX "ind1" ON simplex.test_table (keys(b)); + + Name: ind1 + Target: keys(b) + + CREATE INDEX "ind2" ON simplex.test_table (values(b)); + + Name: ind2 + Target: values(b) """ @cassandra-version-specific @cassandra-version-2.1 @@ -185,3 +363,61 @@ Feature: Schema Metadata State function: avgstate Final function: avgfinal """ + + @cassandra-version-specific @cassandra-version-3.0 + Scenario: Getting materialized view metadata + Given the following schema: + """cql + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}; + CREATE TABLE simplex.test_table (f1 int PRIMARY KEY, f2 int, f3 int) ; + CREATE MATERIALIZED VIEW simplex.test_view AS + SELECT f1, f2 FROM simplex.test_table WHERE f2 IS NOT NULL + PRIMARY KEY (f1, f2); + """ + And the following example: + """ruby + require 'cassandra' + + cluster = Cassandra.cluster + + mv_meta = cluster.keyspace('simplex').materialized_view('test_view') + puts "Name: #{mv_meta.name}" + puts "Base table: #{mv_meta.base_table.name}" + puts "Keyspace: #{mv_meta.keyspace.name}" + puts "Partition key: #{mv_meta.partition_key[0].name}" + puts "Clustering column: #{mv_meta.clustering_columns[0].name}" + puts "Num columns: #{mv_meta.columns.size}" + + puts "" + puts mv_meta.to_cql + """ + When it is executed + Then its output should contain: + """cql + Name: test_view + Base table: test_table + Keyspace: simplex + Partition key: f1 + Clustering column: f2 + Num columns: 2 + + CREATE MATERIALIZED VIEW simplex.test_view AS + SELECT "f1", "f2" + FROM simplex.test_table + WHERE f2 IS NOT NULL + PRIMARY KEY (("f1"), "f2") + WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + """ diff --git a/features/debugging/README.md b/features/debugging/README.md index 948532978..cc8a4b70d 100644 --- a/features/debugging/README.md +++ b/features/debugging/README.md @@ -1 +1,7 @@ -# Debugging \ No newline at end of file +# Debugging + +Execution info can be used to dig into details on how requests executed. If request tracing is enabled, the +trace info will be available in the exection info. + +In addition, configuring the logger for the cluster object will result in extra debug output emitted that can help +track down issues. diff --git a/features/debugging/execution_info.feature b/features/debugging/execution_info.feature index a5c48a36b..f18574929 100644 --- a/features/debugging/execution_info.feature +++ b/features/debugging/execution_info.feature @@ -83,9 +83,11 @@ Feature: Execution information '(id, title, album, artist, tags) values ' + '(?, ?, ?, ?, ?)') - args = [Cassandra::Uuid.new('f6071e72-48ec-4fcb-bf3e-379c8d696488'), 'a' * 5 * 1025, 'a1', 'a1', Set.new(['t1'])] + args0 = [Cassandra::Uuid.new('f6071e72-48ec-4fcb-bf3e-379c8d696488'), 'a' * 5 * 1025, 'a1', 'a1', Set.new(['t1'])] + args1 = [Cassandra::Uuid.new('f6071e72-48ec-4fcb-bf3e-379c8d696498'), 'a' * 5 * 1025, 'a1', 'a1', Set.new(['t1'])] batch = session.unlogged_batch do |b| - b.add(insert, arguments: args) + b.add(insert, arguments: args0) + b.add(insert, arguments: args1) end execution_info = session.execute(batch).execution_info @@ -94,7 +96,7 @@ Feature: Execution information When it is executed Then its output should match: """ - warnings: Batch of prepared statements for .* is of size .*, exceeding specified threshold of 5120 + warnings: Batch.* for .* is of size .*, exceeding specified threshold of 5120 """ Scenario: execution information reflects retry decision diff --git a/features/load_balancing/README.md b/features/load_balancing/README.md index 13b82acbe..1c467e77c 100644 --- a/features/load_balancing/README.md +++ b/features/load_balancing/README.md @@ -28,5 +28,3 @@ Here is a high level diagram of how a load balancing policy is used during query |<-----------------| | | | | | ``` - -[Check out the Ruby Driver - Load Balancing Screencast](https://academy.datastax.com/demos/datastax-ruby-driver-load-balancing-policies) for a quick and in-depth guide to load balancing with the Ruby Driver. diff --git a/features/load_balancing/datacenter_aware.feature b/features/load_balancing/datacenter_aware.feature index 25f268f89..2758e4bd6 100644 --- a/features/load_balancing/datacenter_aware.feature +++ b/features/load_balancing/datacenter_aware.feature @@ -6,13 +6,14 @@ Feature: Datacenter-aware Round Robin Policy datacenter if necessary. The name of the local datacenter must be supplied by the user. - All known remote hosts will be tried when local nodes are not available. - However, you can configure the exact number of remote hosts that will be used - by passing that number when constructing a policy instance. + By default, this policy will not actually fall back to nodes of a remote datacenter. + You must configure the exact number of remote hosts that will be used + by passing that number when constructing a policy instance. A nil value means an + unlimited number of remote hosts can be potentially used. By default, this policy will not attempt to use remote hosts for local consistencies (`:local_one` or `:local_quorum`), however, it is possible to - change that behavior via constructor. + change that behavior via the constructor. Background: Given a running cassandra cluster in 2 datacenters with 2 nodes in each @@ -84,7 +85,7 @@ Feature: Datacenter-aware Round Robin Policy datacenter = "dc2" policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(load_balancing_policy: policy) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) session = cluster.connect('simplex') hosts_used = 4.times.map do @@ -106,10 +107,11 @@ Feature: Datacenter-aware Round Robin Policy """ruby require 'cassandra' - datacenter = "dc2" - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect('simplex') + datacenter = "dc2" + remotes_to_try = nil + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, remotes_to_try) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) + session = cluster.connect('simplex') hosts_used = 4.times.map do info = session.execute("SELECT * FROM songs").execution_info @@ -135,7 +137,7 @@ Feature: Datacenter-aware Round Robin Policy datacenter = "dc2" remotes_to_try = 1 policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, remotes_to_try) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) session = cluster.connect('simplex') hosts_used = 4.times.map do @@ -153,15 +155,16 @@ Feature: Datacenter-aware Round Robin Policy Used 1 host, with ip 127\.0\.0\.(1|2) """ - Scenario: Requests with local consistencies are not routed to remote datacenters + Scenario: Requests with local consistencies are not routed to remote datacenters by default Given the following example: """ruby require 'cassandra' - datacenter = "dc2" - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect('simplex') + datacenter = "dc2" + remotes_to_try = nil + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, remotes_to_try) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) + session = cluster.connect('simplex') begin session.execute("SELECT * FROM songs", consistency: :local_one) @@ -183,14 +186,15 @@ Feature: Datacenter-aware Round Robin Policy """ruby require 'cassandra' - datacenter = "dc2" - use_remote = true - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, nil, use_remote) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect('simplex') + datacenter = "dc2" + remotes_to_try = nil + use_remote = true + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, remotes_to_try, use_remote) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') hosts_used = 4.times.map do - info = session.execute("SELECT * FROM songs").execution_info + info = session.execute("SELECT * FROM songs", consistency: :local_one).execution_info info.hosts.last.ip end.sort.uniq diff --git a/features/load_balancing/token_aware.feature b/features/load_balancing/token_aware.feature index 6b8aca749..8931a3419 100644 --- a/features/load_balancing/token_aware.feature +++ b/features/load_balancing/token_aware.feature @@ -8,7 +8,7 @@ Feature: Token-aware Load Balancing Policy the right partitioners and replication strategies for a given keyspace and locate replicas for a given statement. - In case replica node(s) cannot be found or reached, this policy fallsback + In case replica node(s) cannot be found or reached, this policy falls back onto the wrapped policy plan. Background: @@ -95,11 +95,11 @@ Feature: Token-aware Load Balancing Policy """ruby require 'cassandra' - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new - policy = Cassandra::LoadBalancing::Policies::TokenAware.new(policy) - cluster = Cassandra.cluster(load_balancing_policy: policy) - session = cluster.connect('simplex') - statement = session.prepare("SELECT token(id) FROM songs WHERE id = ?") + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc2') + policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') + statement = session.prepare("SELECT token(id) FROM songs WHERE id = ?") [ Cassandra::Uuid.new('756716f7-2e54-4715-9f00-91dcbea6cf50'), @@ -115,9 +115,9 @@ Feature: Token-aware Load Balancing Policy When it is executed Then its output should contain: """ - uuid=756716f7-2e54-4715-9f00-91dcbea6cf50 token=-4565826248849633211 replica=127.0.0.2 total=1 - uuid=f6071e72-48ec-4fcb-bf3e-379c8a696488 token=-1176857621403111796 replica=127.0.0.2 total=1 - uuid=fbdf82ed-0063-4796-9c7c-a3d4f47b4b25 token=2440231132048646025 replica=127.0.0.1 total=1 + uuid=756716f7-2e54-4715-9f00-91dcbea6cf50 token=-4565826248849633211 replica=127.0.0.4 total=1 + uuid=f6071e72-48ec-4fcb-bf3e-379c8a696488 token=-1176857621403111796 replica=127.0.0.4 total=1 + uuid=fbdf82ed-0063-4796-9c7c-a3d4f47b4b25 token=2440231132048646025 replica=127.0.0.3 total=1 """ Scenario: Requests are routed according to wrapped policy plan when primary replica is down @@ -125,13 +125,14 @@ Feature: Token-aware Load Balancing Policy """ruby require 'cassandra' - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new - policy = Cassandra::LoadBalancing::Policies::TokenAware.new(policy) - cluster = Cassandra.cluster(load_balancing_policy: policy) - session = cluster.connect('simplex') - statement = session.prepare("SELECT token(id) FROM songs WHERE id = ?") + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc2') + policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') + statement = session.prepare("SELECT token(id) FROM songs WHERE id = ?") [ + Cassandra::Uuid.new('756716f7-2e54-4715-9f00-91dcbea6cf50'), Cassandra::Uuid.new('f6071e72-48ec-4fcb-bf3e-379c8a696488'), Cassandra::Uuid.new('fbdf82ed-0063-4796-9c7c-a3d4f47b4b25') ].each do |uuid| @@ -141,12 +142,13 @@ Feature: Token-aware Load Balancing Policy puts "uuid=#{uuid} token=#{result.first.values.first} replica=#{replica.ip} total=#{total}" end """ - And node 2 is stopped + And node 4 is stopped When it is executed Then its output should contain: """ - uuid=f6071e72-48ec-4fcb-bf3e-379c8a696488 token=-1176857621403111796 replica=127.0.0.1 total=1 - uuid=fbdf82ed-0063-4796-9c7c-a3d4f47b4b25 token=2440231132048646025 replica=127.0.0.1 total=1 + uuid=756716f7-2e54-4715-9f00-91dcbea6cf50 token=-4565826248849633211 replica=127.0.0.3 total=1 + uuid=f6071e72-48ec-4fcb-bf3e-379c8a696488 token=-1176857621403111796 replica=127.0.0.3 total=1 + uuid=fbdf82ed-0063-4796-9c7c-a3d4f47b4b25 token=2440231132048646025 replica=127.0.0.3 total=1 """ Scenario: Requests with compound partition keys are routed to the primary replica @@ -154,7 +156,7 @@ Feature: Token-aware Load Balancing Policy """ruby require 'cassandra' - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc2') policy = Cassandra::LoadBalancing::Policies::TokenAware.new(policy) cluster = Cassandra.cluster(load_balancing_policy: policy) session = cluster.connect('simplex') @@ -175,8 +177,42 @@ Feature: Token-aware Load Balancing Policy When it is executed Then its output should contain: """ - uuid=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d title=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d token=6231549073425362204 replica=127.0.0.1 total=1 - uuid=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d title=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d token=-115815985718975675 replica=127.0.0.2 total=1 - uuid=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 title=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 token=-463065628644986368 replica=127.0.0.2 total=1 - uuid=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 title=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 token=-8087998491924709995 replica=127.0.0.2 total=1 + uuid=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d title=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d token=6231549073425362204 replica=127.0.0.3 total=1 + uuid=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d title=2cc9ccb7-6221-4ccb-8387-f22b6a1b354d token=-115815985718975675 replica=127.0.0.4 total=1 + uuid=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 title=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 token=-463065628644986368 replica=127.0.0.4 total=1 + uuid=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 title=3fd2bedf-a8c8-455a-a462-0cd3a4353c54 token=-8087998491924709995 replica=127.0.0.4 total=1 + """ + + Scenario: Requests with local consistencies are routed to a remote datacenter when primary replica is down + Given the following example: + """ruby + require 'cassandra' + + max_remote_hosts_to_use = nil + use_remote_hosts_for_local_consistency = true + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc2', + max_remote_hosts_to_use, + use_remote_hosts_for_local_consistency) + policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') + statement = session.prepare("SELECT token(id) FROM songs WHERE id = ?") + + [ + Cassandra::Uuid.new('756716f7-2e54-4715-9f00-91dcbea6cf50'), + Cassandra::Uuid.new('f6071e72-48ec-4fcb-bf3e-379c8a696488') + ].each do |uuid| + result = session.execute(statement, arguments: [uuid], consistency: :local_one) + replica = result.execution_info.hosts.first + total = result.execution_info.hosts.size + puts "uuid=#{uuid} token=#{result.first.values.first} replica=#{replica.ip} total=#{total}" + end + """ + And node 3 is stopped + And node 4 is stopped + When it is executed + Then its output should contain: """ + uuid=756716f7-2e54-4715-9f00-91dcbea6cf50 token=-4565826248849633211 replica=127.0.0.2 total=1 + uuid=f6071e72-48ec-4fcb-bf3e-379c8a696488 token=-1176857621403111796 replica=127.0.0.1 total=1 + """ \ No newline at end of file diff --git a/features/reconnection/README.md b/features/reconnection/README.md index 9ae5778f5..d41eb9c8d 100644 --- a/features/reconnection/README.md +++ b/features/reconnection/README.md @@ -1,3 +1,3 @@ # Reconnection -Ruby driver will automatically reestablish failed connections to Cassandra nodes. It uses reconnection policy to determine retry intervals for reconnection. \ No newline at end of file +Ruby driver will automatically reestablish failed connections to Cassandra nodes. It uses a reconnection policy to determine retry intervals for reconnection. \ No newline at end of file diff --git a/features/reconnection/automatic_reconnection.feature b/features/reconnection/automatic_reconnection.feature index 504b7dcd4..c50164edf 100644 --- a/features/reconnection/automatic_reconnection.feature +++ b/features/reconnection/automatic_reconnection.feature @@ -100,6 +100,7 @@ Feature: Automatic reconnection And it is running interactively And I wait for its output to contain "START" + @interactive Scenario: Driver reconnects when all hosts are down When node 1 stops When node 2 stops diff --git a/features/state_listeners/README.md b/features/state_listeners/README.md index 46a41b3ca..765f7e5b3 100644 --- a/features/state_listeners/README.md +++ b/features/state_listeners/README.md @@ -1 +1,3 @@ -# State Listeners \ No newline at end of file +# State Listeners + +You can define listeners for schema change events as well as cluster membership changes. diff --git a/features/state_listeners/membership_changes.feature b/features/state_listeners/membership_changes.feature index 275bcc0d1..1bbfa27a5 100644 --- a/features/state_listeners/membership_changes.feature +++ b/features/state_listeners/membership_changes.feature @@ -1,7 +1,7 @@ -Feature: membership change detection +Feature: Membership change detection Cluster object allows registering state listeners. It then guarantees that - they will be notifies on cluster membership changes. + they will be notified on cluster membership changes. Background: Given a running cassandra cluster @@ -39,6 +39,8 @@ Feature: membership change detection cluster.register(listener) + session = cluster.connect + $stdout.puts("=== START ===") $stdout.flush $stdin.gets @@ -69,6 +71,9 @@ Feature: membership change detection """ Host 127.0.0.4 is found Host 127.0.0.4 is up + """ + And its output should contain: + """ Host 127.0.0.4 is down Host 127.0.0.4 is lost """ diff --git a/features/support/env.rb b/features/support/env.rb index c3194bb2c..2fcb914e2 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -56,6 +56,7 @@ def to_str Aruba.configure do |config| config.exit_timeout = 60 + config.remove_ansi_escape_sequences = false end Before do @@ -73,7 +74,7 @@ def to_str end After('@ssl') do - @cluster.disable_ssl + CCM.remove_cluster(@cluster.name) end After('@netblock') do @@ -82,4 +83,105 @@ def to_str After('@client_failures') do @cluster.restart -end \ No newline at end of file +end + +############################################################################################################## +# The following matchers and aruba SpawnProcess monkey-patch cause aruba to write command output to file +# with binmode to avoid encoding / translation issues. The matchers take the "binary" output and re-interpret +# as utf8. This works around (resolves?) a behavior difference in jruby9k where cukes were failing when output +# included non-ascii characters. RUBY-167 +############################################################################################################## + +RSpec::Matchers.define :include_output_string do |expected| + match do |actual| + actual.force_encoding("UTF-8") + @expected = Regexp.new(Regexp.escape(sanitize_text(expected.to_s)), Regexp::MULTILINE) + @actual = sanitize_text(actual) + + values_match? @expected, @actual + end + + diffable + + description { "string includes: #{description_of expected}" } +end + +RSpec::Matchers.define :match_output_string do |expected| + match do |actual| + actual.force_encoding("UTF-8") + @expected = Regexp.new(unescape_text(expected), Regexp::MULTILINE) + @actual = sanitize_text(actual) + + values_match? @expected, @actual + end + + diffable + + description { "output string matches: #{description_of expected}" } +end + +RSpec::Matchers.define :output_string_eq do |expected| + match do |actual| + actual.force_encoding("UTF-8") + @expected = sanitize_text(expected.to_s) + @actual = sanitize_text(actual.to_s) + + values_match? @expected, @actual + end + + diffable + + description { "output string is eq: #{description_of expected}" } +end + +module Aruba + module Processes + class SpawnProcess + def start + # rubocop:disable Metrics/LineLength + fail CommandAlreadyStartedError, %(Command "#{commandline}" has already been started. Please `#stop` the command first and `#start` it again. Alternatively use `#restart`.\n#{caller.join("\n")}) if started? + # rubocop:enable Metrics/LineLength + + @started = true + + @process = ChildProcess.build(*[command_string.to_a, arguments].flatten) + @stdout_file = Tempfile.new('aruba-stdout-') + @stderr_file = Tempfile.new('aruba-stderr-') + + + @stdout_file.sync = true + @stderr_file.sync = true + + @stdout_file.binmode + @stderr_file.binmode + + @exit_status = nil + @duplex = true + + before_run + + @process.leader = true + @process.io.stdout = @stdout_file + @process.io.stderr = @stderr_file + @process.duplex = @duplex + @process.cwd = @working_directory + + @process.environment.update(environment) + + begin + Aruba.platform.with_environment(environment) do + @process.start + sleep startup_wait_time + end + rescue ChildProcess::LaunchError => e + raise LaunchError, "It tried to start #{cmd}. " + e.message + end + + after_run + + yield self if block_given? + end + end + end +end + diff --git a/integration/client_error_test.rb b/integration/client_error_test.rb index 4d38ed068..e0cbda7ec 100644 --- a/integration/client_error_test.rb +++ b/integration/client_error_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -69,32 +69,43 @@ def set_failing_nodes(failing_nodes, keyspace) # @test_category error_codes # def test_raise_error_on_write_failure - begin - skip("Client failure errors are only available in C* after 2.2") if CCM.cassandra_version < '2.2.0' - - cluster = Retry.with_attempts(5, Cassandra::Errors::NoHostsAvailable) { Cassandra.cluster } - session = cluster.connect + skip("Client failure errors are only available in C* after 2.2") if CCM.cassandra_version < '2.2.0' - session.execute("CREATE KEYSPACE testwritefail WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}", - consistency: :all) rescue nil - session.execute("CREATE TABLE testwritefail.test (k int PRIMARY KEY, v int)", consistency: :all) + # Create a cluster object that tries to use the beta protocol. If it succeeds, the write failure response + # will have a map of instead of num-failures. When v5 is officially released, we + # can remove the allow_beta_protocol arg in this test. + cluster = Retry.with_attempts(5, Cassandra::Errors::NoHostsAvailable) { + Cassandra.cluster + } + session = cluster.connect - # Disable one node - set_failing_nodes(["node1"], "testwritefail") + session.execute("CREATE KEYSPACE testwritefail WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}", + consistency: :all) rescue nil + session.execute("CREATE TABLE IF NOT EXISTS testwritefail.test (k int PRIMARY KEY, v int)", consistency: :all) - # One node disabled should trigger a WriteFailure - assert_raises(Cassandra::Errors::WriteError) do - session.execute("INSERT INTO testwritefail.test (k, v) VALUES (1, 0)", consistency: :all) - end + # Disable one node + set_failing_nodes(["node1"], "testwritefail") - # Quorum should still work with two nodes - session.execute("INSERT INTO testwritefail.test (k, v) VALUES (1, 0)", consistency: :quorum) + # One node disabled should trigger a WriteFailure + ex = assert_raises(Cassandra::Errors::WriteError) do + session.execute("INSERT INTO testwritefail.test (k, v) VALUES (1, 0)", consistency: :all) + end - # Restart the node to clear jvm settings - set_failing_nodes([], "testwritefail") - ensure - cluster && cluster.close + # If we're speaking protocol version > 4, verify that we have a failure map in the exception. + connection_options = cluster.instance_variable_get(:@connection_options) + assert_operator(ex.failed, :>=, 1) + if connection_options.protocol_version > 4 + assert_equal(ex.failed, ex.failures_by_node.size) + assert_equal(0, ex.failures_by_node.values.first) end + + # Quorum should still work with two nodes + session.execute("INSERT INTO testwritefail.test (k, v) VALUES (1, 0)", consistency: :quorum) + + # Restart the node to clear jvm settings + set_failing_nodes([], "testwritefail") + ensure + cluster && cluster.close end # Test for validating ReadError @@ -117,33 +128,45 @@ def test_raise_error_on_write_failure def test_raise_error_on_read_failure skip("Client failure errors are only available in C* after 2.2") if CCM.cassandra_version < '2.2.0' - begin - cluster = Retry.with_attempts(5, Cassandra::Errors::NoHostsAvailable) { Cassandra.cluster } - session = cluster.connect - - session.execute("CREATE KEYSPACE testreadfail WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}", - consistency: :all) rescue nil - session.execute("CREATE TABLE testreadfail.test2 (k int, v0 int, v1 int, PRIMARY KEY (k, v0))", consistency: :all) + # Create a cluster object that tries to use the beta protocol. If it succeeds, the write failure response + # will have a map of instead of num-failures. When v5 is officially released, we + # can remove the allow_beta_protocol arg in this test. + cluster = Retry.with_attempts(5, Cassandra::Errors::NoHostsAvailable) { + Cassandra.cluster + } + session = cluster.connect + + session.execute("CREATE KEYSPACE testreadfail WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}", + consistency: :all) rescue nil + session.execute("CREATE TABLE IF NOT EXISTS testreadfail.test2 (k int, v0 int, v1 int, PRIMARY KEY (k, v0))", + consistency: :all) + + # Insert wide rows + insert = session.prepare("INSERT INTO testreadfail.test2 (k, v0, v1) VALUES (1, ?, 1)") + (0..3000).each do |num| + session.execute(insert, arguments: [num]) + end - # Insert wide rows - insert = session.prepare("INSERT INTO testreadfail.test2 (k, v0, v1) VALUES (1, ?, 1)") - (0..3000).each do |num| - session.execute(insert, arguments: [num]) - end + # Delete wide rows + delete = session.prepare("DELETE v1 FROM testreadfail.test2 WHERE k = 1 AND v0 =?") + (0..2001).each do |num| + session.execute(delete, arguments: [num]) + end - # Delete wide rows - delete = session.prepare("DELETE v1 FROM testreadfail.test2 WHERE k = 1 AND v0 =?") - (0..2001).each do |num| - session.execute(delete, arguments: [num]) - end + # Tombstones should trigger ReadFailure + ex = assert_raises(Cassandra::Errors::ReadError) do + session.execute("SELECT * FROM testreadfail.test2 WHERE k = 1") + end - # Tombstones should trigger ReadFailure - assert_raises(Cassandra::Errors::ReadError) do - session.execute("SELECT * FROM testreadfail.test2 WHERE k = 1") - end - ensure - cluster && cluster.close + # If we're speaking protocol version > 4, verify that we have a failure map in the exception. + connection_options = cluster.instance_variable_get(:@connection_options) + assert_operator(ex.failed, :>=, 1) + if connection_options.protocol_version > 4 + assert_equal(ex.failed, ex.failures_by_node.size) + assert_equal(1, ex.failures_by_node.values.first) end + ensure + cluster && cluster.close end # Test for validating FunctionCallError @@ -165,30 +188,28 @@ def test_raise_error_on_read_failure def test_raise_error_on_function_failure skip("Client failure errors are only available in C* after 2.2") if CCM.cassandra_version < '2.2.0' - begin - cluster = Retry.with_attempts(5, Cassandra::Errors::NoHostsAvailable) { Cassandra.cluster } - session = cluster.connect + cluster = Retry.with_attempts(5, Cassandra::Errors::NoHostsAvailable) { Cassandra.cluster } + session = cluster.connect - session.execute("CREATE KEYSPACE testfunctionfail WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}", - consistency: :all) rescue nil - session.execute("CREATE TABLE testfunctionfail.d (k int PRIMARY KEY , d double)", consistency: :all) + session.execute("CREATE KEYSPACE testfunctionfail WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}", + consistency: :all) rescue nil + session.execute("CREATE TABLE IF NOT EXISTS testfunctionfail.d (k int PRIMARY KEY , d double)", consistency: :all) - # Create a UDF that throws an exception - session.execute("CREATE FUNCTION testfunctionfail.test_failure(d double) + # Create a UDF that throws an exception + session.execute("CREATE FUNCTION IF NOT EXISTS testfunctionfail.test_failure(d double) RETURNS NULL ON NULL INPUT RETURNS double LANGUAGE java AS 'throw new RuntimeException(\"failure\");'", - consistency: :all) + consistency: :all) - # Insert value to use for function - session.execute("INSERT INTO testfunctionfail.d (k, d) VALUES (0, 5.12)") + # Insert value to use for function + session.execute("INSERT INTO testfunctionfail.d (k, d) VALUES (0, 5.12)") - # FunctionFailure should be triggered - assert_raises(Cassandra::Errors::FunctionCallError) do - session.execute("SELECT test_failure(d) FROM testfunctionfail.d WHERE k = 0") - end - ensure - cluster && cluster.close + # FunctionFailure should be triggered + assert_raises(Cassandra::Errors::FunctionCallError) do + session.execute("SELECT test_failure(d) FROM testfunctionfail.d WHERE k = 0") end + ensure + cluster && cluster.close end end diff --git a/integration/client_warnings_test.rb b/integration/client_warnings_test.rb index 12c8f3d21..7b4038f6d 100644 --- a/integration/client_warnings_test.rb +++ b/integration/client_warnings_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -37,10 +37,13 @@ def setup CREATE TABLE test (k int, v text, PRIMARY KEY (k, v)); CQL - @query = "BEGIN UNLOGGED BATCH INSERT INTO test (k, v) VALUES (0, '#{'a' * 5 * 1025}') APPLY BATCH" - @exceeding_warning = Regexp.new(/Batch of prepared statements for .* is of size .*, exceeding specified threshold of 5120/) - @partition_warning = Regexp.new(/Unlogged batch covering 2 partitions detected against table .* You should use a logged \ -batch for atomicity, or asynchronous writes for performance/) + @query = "BEGIN UNLOGGED BATCH + INSERT INTO test (k, v) VALUES (0, '#{'a' * 5 * 1025}') + INSERT INTO test (k, v) VALUES (1, '#{'a' * 5 * 1025}') + APPLY BATCH" + @exceeding_warning = Regexp.new(/Batch.* for .* is of size .*, exceeding specified threshold of 5120/) + @partition_warning = Regexp.new(/Unlogged batch covering 2 partitions detected against table .* You should use a \ +logged batch for atomicity, or asynchronous writes for performance/) end end @@ -167,4 +170,4 @@ def test_batch_exceeding_length_with_trace_and_custom_payload assert_match @exceeding_warning, execution_info.warnings.first assert_equal custom_payload, execution_info.payload end -end \ No newline at end of file +end diff --git a/integration/control_connection_test.rb b/integration/control_connection_test.rb new file mode 100644 index 000000000..ca1f1a681 --- /dev/null +++ b/integration/control_connection_test.rb @@ -0,0 +1,130 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require File.dirname(__FILE__) + '/integration_test_case.rb' + +class ControlConnectionTest < IntegrationTestCase + + def self.before_suite + @@ccm_cluster = CCM.setup_cluster(1, 2) + end + + def self.after_suite + CCM.remove_cluster(@@ccm_cluster.name) + end + + def remove_peer_info(info) + # Make sure to only connect to node1, as each node has its own peers table + allowed_ips = ['127.0.0.1'] + round_robin = Cassandra::LoadBalancing::Policies::RoundRobin.new + whitelist = Cassandra::LoadBalancing::Policies::WhiteList.new(allowed_ips, round_robin) + cluster = Cassandra.cluster(load_balancing_policy: whitelist) + session = cluster.connect + + value = session.execute("SELECT #{info} FROM system.peers WHERE peer = '127.0.0.2'").first[info] + session.execute("DELETE #{info} FROM system.peers WHERE peer = '127.0.0.2'") + result = session.execute("SELECT #{info} FROM system.peers WHERE peer = '127.0.0.2'").first + assert_nil result[info] + + cluster.close + value + end + + def restore_peer_info(info, value) + # Make sure to only connect to node1, as each node has its own peers table + allowed_ips = ['127.0.0.1'] + round_robin = Cassandra::LoadBalancing::Policies::RoundRobin.new + whitelist = Cassandra::LoadBalancing::Policies::WhiteList.new(allowed_ips, round_robin) + cluster = Cassandra.cluster(load_balancing_policy: whitelist) + session = cluster.connect + + session.execute("UPDATE system.peers SET #{info}=? WHERE peer = '127.0.0.2'", arguments: [value]) + result = session.execute("SELECT #{info} FROM system.peers WHERE peer = '127.0.0.2'").first + assert_equal value, result[info] + + cluster.close + end + + # Test for null columns in peer + # + # test_missing_peer_columns tests that the control connection ignores any peers which have missing peer columns. + # Using a simple 2-node cluster, it first removes one of the peer columns of node2 from node1's system.peers + # table. It then uses node1 explicitly as the control connection and verifies that node2 has not been used as a host. + # It finally restores the peer column in node1 so the next test case can continue.g + # + # @since 2.1.7 + # @jira_ticket RUBY-255 + # @expected_result Node2 should not be used as a host + # + # @test_assumptions A 2-node Cassandra cluster. + # @test_category control_connection + # + def test_missing_peer_columns + peer_info = ['host_id', 'data_center', 'rack', 'rpc_address', 'tokens'] + + peer_info.each do |info| + begin + original_value = remove_peer_info(info) + cluster = Cassandra.cluster(hosts: ['127.0.0.1']) + assert_equal ['127.0.0.1'], cluster.hosts.map { |h| h.ip.to_s } + cluster.close + ensure + restore_peer_info(info, original_value) + end + end + end + + # Test for repreparing statements on another host + # + # test_can_reprepare_statements_automatically tests that prepared statements are automatically reprepared on a host + # if that host does not already have the prepared statement in its cache. It first creates a simple keyspace + # and table to be used. It then prepares an insert statement on node2, by keeping node1 down. It then brings node1 + # back up but brings node2 down. Finally it executes the prepared statement on node1, and verifies that the query + # is executed successfully using node1. + # + # @since 3.1.0 + # @jira_ticket RUBY-257 + # @expected_result Node1 should be able to be used to execute the prepared statement + # + # @test_assumptions A 2-node Cassandra cluster. + # @test_category prepared_statements:preparation + # + def test_can_reprepare_statements_automatically + cluster = Cassandra.cluster + session = cluster.connect + + session.execute("CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2}") + session.execute("USE simplex") + session.execute("CREATE TABLE test (k int, v int, PRIMARY KEY (k, v))") + + # Prepare on node2 + @@ccm_cluster.stop_node('node1') + insert = session.prepare("INSERT INTO test (k,v) VALUES (?, ?)") + assert_equal 1, insert.execution_info.hosts.size + assert_equal '127.0.0.2', insert.execution_info.hosts.first.ip.to_s + @@ccm_cluster.start_node('node1') + + # Insert using node1 + @@ccm_cluster.stop_node('node2') + info = session.execute(insert, arguments: [0,0]).execution_info + assert_equal 1, info.hosts.size + assert_equal '127.0.0.1', info.hosts.first.ip.to_s + ensure + cluster.close + end +end diff --git a/integration/custom_payload_test.rb b/integration/custom_payload_test.rb index bb88c26aa..4ffa673fa 100644 --- a/integration/custom_payload_test.rb +++ b/integration/custom_payload_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/integration/datatype_utils.rb b/integration/datatype_utils.rb index 379b79acf..01ba254b4 100644 --- a/integration/datatype_utils.rb +++ b/integration/datatype_utils.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -98,4 +98,137 @@ def self.get_collection_sample(complex_type, datatype) end end + def self.reserved_words + @@reserved_words ||= begin + reserved_words = Array.new(%w( + add + aggregate + all + allow + alter + and + apply + as + asc + ascii + authorize + batch + begin + bigint + blob + boolean + by + called + clustering + columnfamily + compact + contains + count + counter + create + custom + date + decimal + delete + desc + describe + distinct + double + drop + entries + execute + exists + filtering + finalfunc + float + from + frozen + full + function + functions + grant + if + in + index + inet + infinity + initcond + input + insert + int + into + is + json + key + keys + keyspace + keyspaces + language + limit + list + login + map + materialized + modify + nan + nologin + norecursive + nosuperuser + not + null + of + on + options + or + order + password + permission + permissions + primary + rename + replace + returns + revoke + role + roles + schema + select + set + sfunc + smallint + static + storage + stype + superuser + table + text + time + timestamp + timeuuid + tinyint + to + token + trigger + truncate + ttl + tuple + type + unlogged + update + use + user + users + using + uuid + values + varchar + varint + view + where + with + writetime + )) + end + end + end diff --git a/integration/functions/user_defined_aggregate_test.rb b/integration/functions/user_defined_aggregate_test.rb index a8d1bdcfa..26bde34e5 100644 --- a/integration/functions/user_defined_aggregate_test.rb +++ b/integration/functions/user_defined_aggregate_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ #++ require File.dirname(__FILE__) + '/../integration_test_case.rb' -require_relative 'schema_change_listener' # noinspection RubyInstanceMethodNamingConvention class UserDefinedAggregateTest < IntegrationTestCase @@ -43,19 +42,19 @@ def setup RETURNS map LANGUAGE java AS 'if (state.get(star_rating) == null) state.put(star_rating, 1); else state.put(star_rating, ((Integer) state.get(star_rating)) + 1); return state;'; - CREATE FUNCTION state_group_and_sum(state map, star_rating smallint) + CREATE FUNCTION state_group_and_sum(state map, star_rating smallint) CALLED ON NULL INPUT - RETURNS map + RETURNS map LANGUAGE java AS 'if (state.get(star_rating) == null) state.put(star_rating, 1); else state.put(star_rating, ((Integer) state.get(star_rating)) + 1); return state;'; CREATE FUNCTION percent_stars(state map) RETURNS NULL ON NULL INPUT RETURNS map LANGUAGE java AS 'Integer sum = 0; for(Object k : state.keySet()) { sum = sum + (Integer) state.get((Integer) k); } java.util.Map results = new java.util.HashMap(); for(Object k : state.keySet()) { results.put((Integer) k, ((Integer) state.get((Integer) k))*100 / sum); } return results;'; - CREATE FUNCTION percent_stars(state map) + CREATE FUNCTION percent_stars(state map) RETURNS NULL ON NULL INPUT - RETURNS map - LANGUAGE java AS 'Integer sum = 0; for(Object k : state.keySet()) { sum = sum + (Integer) state.get((Integer) k); } java.util.Map results = new java.util.HashMap(); for(Object k : state.keySet()) { results.put((Integer) k, ((Integer) state.get((Integer) k))*100 / sum); } return results;'; + RETURNS map + LANGUAGE java AS 'Integer sum = 0; for(Object k : state.keySet()) { sum = sum + (Integer) state.get((Short) k); } java.util.Map results = new java.util.HashMap(); for(Object k : state.keySet()) { results.put((Short) k, ((Integer) state.get((Short) k))*100 / sum); } return results;'; CREATE FUNCTION extend_list(s list, i int) CALLED ON NULL INPUT RETURNS list @@ -244,17 +243,17 @@ def test_udas_with_finalfunc # verify that we pick the right final func. @session.execute('CREATE OR REPLACE AGGREGATE group_and_sum2(smallint) SFUNC state_group_and_sum - STYPE map + STYPE map FINALFUNC percent_stars INITCOND NULL' ) @listener.wait_for_aggregate('simplex', 'group_and_sum2', smallint) aggregate = @cluster.keyspace('simplex').aggregate('group_and_sum2', smallint) - assert @cluster.keyspace('simplex').has_function?('state_group_and_sum', map(int, smallint), smallint) - state_function = @cluster.keyspace('simplex').function('state_group_and_sum', map(int, smallint), smallint) - assert @cluster.keyspace('simplex').has_function?('percent_stars', map(int, smallint)) - final_function = @cluster.keyspace('simplex').function('percent_stars', map(int, smallint)) + assert @cluster.keyspace('simplex').has_function?('state_group_and_sum', map(smallint, int), smallint) + state_function = @cluster.keyspace('simplex').function('state_group_and_sum', map(smallint, int), smallint) + assert @cluster.keyspace('simplex').has_function?('percent_stars', map(smallint, int)) + final_function = @cluster.keyspace('simplex').function('percent_stars', map(smallint, int)) assert_equal state_function, aggregate.state_function assert_equal final_function, aggregate.final_function diff --git a/integration/functions/user_defined_function_test.rb b/integration/functions/user_defined_function_test.rb index 84ec47131..9a1ab08dc 100644 --- a/integration/functions/user_defined_function_test.rb +++ b/integration/functions/user_defined_function_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ #++ require File.dirname(__FILE__) + '/../integration_test_case.rb' -require_relative 'schema_change_listener' # noinspection RubyInstanceMethodNamingConvention class UserDefinedFunctionTest < IntegrationTestCase diff --git a/integration/idempotency_test.rb b/integration/idempotency_test.rb index 9689dacf6..d3500d4ce 100644 --- a/integration/idempotency_test.rb +++ b/integration/idempotency_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,12 +20,12 @@ class IdempotencyTest < IntegrationTestCase def self.before_suite - @@ccm_cluster = CCM.setup_cluster(1, 2) + @@ccm_cluster = CCM.setup_cluster(2, 1) end def setup @@ccm_cluster.setup_schema(<<-CQL) - CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2}; + CREATE KEYSPACE simplex WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': '1', 'dc2': '1'}; USE simplex; CREATE TABLE test (k int, v int, PRIMARY KEY (k, v)); INSERT INTO test (k, v) VALUES (0, 0) @@ -37,7 +37,7 @@ def setup # test_statement_idempotency_on_timeout tests that idempotent statements are retried automatically on the next host. # It first blocks the first host such that it is unreachable. It then attempts a simple SELECT query and verifies that # a Cassandra::Errors::TimeoutError is raised, and the next host is not tried. It finally executes the same query - # once more with idempotent: true and verifies that the statement executes successfully. + # once more with idempotent: true and verifies that the statement executes successfully on another host. # # @expected_errors [Cassandra::Errors::TimeoutError] When a host is unavailable on a non-idempotent query # @@ -45,23 +45,85 @@ def setup # @jira_ticket RUBY-146 # @expected_result Idempotent queries should be retried on the next host automatically # - # @test_assumptions A 2-node Cassandra cluster with RF=2. + # @test_assumptions A 2-dc Cassandra cluster 1 node in each dc. # @test_category queries:timeout # def test_statement_idempotency_on_timeout - cluster = Cassandra.cluster - session = cluster.connect("simplex") + datacenter = 'dc1' + max_remote_hosts_to_use = 1 + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') - @@ccm_cluster.block_node("node1") + @@ccm_cluster.block_node('node1') assert_raises(Cassandra::Errors::TimeoutError) do - session.execute("SELECT * FROM test", consistency: :one) + session.execute('SELECT * FROM test', consistency: :one) end - session.execute("SELECT * FROM test", consistency: :one, idempotent: true) + info = nil + begin + info = session.execute('SELECT * FROM test', consistency: :one, idempotent: true).execution_info + assert_equal 1, info.retries + assert_equal 2, info.hosts.size + assert_equal '127.0.0.1', info.hosts[0].ip.to_s + assert_equal '127.0.0.2', info.hosts[1].ip.to_s + rescue Cassandra::Errors::ReadTimeoutError => e + # Every once in a while, the test fails with a ReadTimeoutError. Try to report extra info in that case that + # may help us track down the core issue. + info = e.execution_info + assert_equal('', info.inspect, 'Got ReadTimeoutError when statement should have succeeded') + end ensure @@ccm_cluster.unblock_nodes cluster && cluster.close end + # Test for retrying idempotent statements on timeout + # + # test_statement_idempotency_on_timeout tests that idempotent statements are retried automatically on the next host, + # when the keyspace is not predefined. It first blocks the first host such that it is unreachable. It then attempts a + # simple SELECT query and verifies that a Cassandra::Errors::TimeoutError is raised, and the next host is not tried. + # It finally executes the same query once more with idempotent: true and verifies that the statement executes + # successfully on another host. + # + # @expected_errors [Cassandra::Errors::TimeoutError] When a host is unavailable on a non-idempotent query + # + # @since 3.0.1 + # @jira_ticket RUBY-233 + # @expected_result Idempotent queries should be retried on the next host automatically + # + # @test_assumptions A 2-dc Cassandra cluster 1 node in each dc. + # @test_category queries:timeout + # + def test_statement_idempotency_on_timeout_no_keyspace_predefined + datacenter = 'dc1' + max_remote_hosts_to_use = 1 + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect + + @@ccm_cluster.block_node('node1') + + assert_raises(Cassandra::Errors::TimeoutError) do + session.execute('SELECT * FROM simplex.test', consistency: :one) + end + + info = nil + begin + info = session.execute('SELECT * FROM simplex.test', consistency: :one, idempotent: true).execution_info + assert_equal 1, info.retries + assert_equal 2, info.hosts.size + assert_equal '127.0.0.1', info.hosts[0].ip.to_s + assert_equal '127.0.0.2', info.hosts[1].ip.to_s + rescue Cassandra::Errors::ReadTimeoutError => e + # Every once in a while, the test fails with a ReadTimeoutError. Try to report extra info in that case that + # may help us track down the core issue. + info = e.execution_info + assert_equal('', info.inspect, 'Got ReadTimeoutError when statement should have succeeded') + end + ensure + @@ccm_cluster.unblock_nodes + cluster && cluster.close + end end diff --git a/integration/indexes/indexes_test.rb b/integration/indexes/indexes_test.rb new file mode 100644 index 000000000..8d09627fa --- /dev/null +++ b/integration/indexes/indexes_test.rb @@ -0,0 +1,181 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require File.dirname(__FILE__) + '/../integration_test_case.rb' + +class IndexesTest < IntegrationTestCase + + def setup + @@ccm_cluster.setup_schema("CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}") + + @cluster = Cassandra.cluster( + schema_refresh_delay: 0.1, + schema_refresh_timeout: 0.1 + ) + @listener = SchemaChangeListener.new(@cluster) + @session = @cluster.connect('simplex') + end + + def teardown + @cluster && @cluster.close + end + + # Test for creating indexes + # + # test_can_create_index tests that indexes can be created using the driver. It first creates a simple table and a + # simple index on that table. It then verifies that the index metadata is being properly retrieved. + # + # @since 3.0.0 + # @jira_ticket RUBY-178 + # @expected_result index should be created and its metadata should be retrieved. + # + # @test_category indexes + # + def test_can_create_index + @session.execute("CREATE TABLE simplex.test (a text PRIMARY KEY, b text)") + @session.execute("CREATE INDEX b_index ON simplex.test (b)") + + @listener.wait_for_index('simplex', 'test', 'b_index') + + assert @cluster.keyspace('simplex').table('test').has_index?('b_index') + index = @cluster.keyspace('simplex').table('test').index('b_index') + assert @cluster.keyspace('simplex').has_index?('b_index') + assert_same(index, @cluster.keyspace('simplex').index('b_index')) + assert_equal 'b_index', index.name + assert_equal 'test', index.table.name + assert_equal :composites, index.kind + assert_equal 'b', index.target + end + + # Test for creating indexes on partial collections + # + # test_can_create_index_on_partial_collections tests that indexes can be created on partial (non-frozen) collections. + # The indexes can be created on either the keys or the values, but not both at the same time. It first creates a table + # and an index on the keys of the collection, verifying the metadata once created. It then drops the original index and + # creates another index, but this time on the values of the collection. + # + # @since 3.0.0 + # @jira_ticket RUBY-178 + # @expected_result partial collection indexes should be created and their metadata should be retrieved. + # + # @test_category indexes + # + def test_can_create_index_on_partial_collections + skip("Secondary index on partial collections were introduced in Cassandra 2.1") if CCM.cassandra_version < '2.1.0' + + @session.execute("CREATE TABLE simplex.collection_test (a int PRIMARY KEY, b map)") + @session.execute("CREATE INDEX b_index ON simplex.collection_test (keys(b))") + + @listener.wait_for_index('simplex', 'collection_test', 'b_index') + + assert @cluster.keyspace('simplex').table('collection_test').has_index?('b_index') + index = @cluster.keyspace('simplex').table('collection_test').index('b_index') + assert_equal 'b_index', index.name + assert_equal 'collection_test', index.table.name + assert_equal :composites, index.kind + assert_equal 'keys(b)', index.target + + @session.execute("DROP INDEX b_index") + @cluster.refresh_schema + @session.execute("CREATE INDEX b_index ON simplex.collection_test (b)") + + @listener.wait_for_index('simplex', 'collection_test', 'b_index') + + assert @cluster.keyspace('simplex').table('collection_test').has_index?('b_index') + index = @cluster.keyspace('simplex').table('collection_test').index('b_index') + assert_equal 'b_index', index.name + assert_equal 'collection_test', index.table.name + assert_equal :composites, index.kind + if CCM.cassandra_version < '3.0.0' + assert_equal 'b', index.target + else + assert_equal 'values(b)', index.target + end + end + + # Test for creating indexes on full collections + # + # test_can_create_index_on_full_collections tests that indexes can be created on full (frozen) collections. It first + # creates a table which includes a frozen collection. It then creates a full index on the collection, verifying that + # the metadata is properly retrieved. + # + # @since 3.0.0 + # @jira_ticket RUBY-178 + # @expected_result full collection indexes should be created and their metadata should be retrieved. + # + # @test_category indexes + # + def test_can_create_index_on_full_collections + skip("Secondary index on full collections were introduced in Cassandra 2.1.3") if Gem::Version.new(CCM.cassandra_version) < Gem::Version.new('2.1.3') + + @session.execute("CREATE TABLE simplex.collection_test (a int PRIMARY KEY, b frozen>)") + @session.execute("CREATE INDEX b_index ON simplex.collection_test (full(b))") + + @listener.wait_for_index('simplex', 'collection_test', 'b_index') + + assert @cluster.keyspace('simplex').table('collection_test').has_index?('b_index') + index = @cluster.keyspace('simplex').table('collection_test').index('b_index') + assert_equal 'b_index', index.name + assert_equal 'collection_test', index.table.name + assert_equal :composites, index.kind + if CCM.cassandra_version < '3.0.0' + assert_equal 'b', index.target + else + assert_equal 'full(b)', index.target + end + end + + # Test for creating multiple indexes on the same column + # + # test_can_create_multiple_indexes_same_column tests that multiple indexes can be created on the same column. It first + # creates a table which includes a non-frozen collection. It then creates two indexes: one for the key and another for + # the value of the collection. It then verifies the metadata associated with each index. + # + # @since 3.0.0 + # @jira_ticket RUBY-178 + # @expected_result multiple indexes should be created and their metadata should be retrieved. + # + # @test_category indexes + # + def test_can_create_multiple_indexes_same_column + skip("Multiple indexes on same column were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + @session.execute("CREATE TABLE simplex.multi_index_test (a int PRIMARY KEY, b map)") + @session.execute("CREATE INDEX key_index ON simplex.multi_index_test (keys(b))") + @session.execute("CREATE INDEX value_index ON simplex.multi_index_test (values(b))") + + @listener.wait_for_index('simplex', 'multi_index_test', 'key_index') + @listener.wait_for_index('simplex', 'multi_index_test', 'value_index') + + assert @cluster.keyspace('simplex').table('multi_index_test').has_index?('key_index') + assert @cluster.keyspace('simplex').table('multi_index_test').has_index?('value_index') + + key_index = @cluster.keyspace('simplex').table('multi_index_test').index('key_index') + assert_equal 'key_index', key_index.name + assert_equal 'multi_index_test', key_index.table.name + assert_equal :composites, key_index.kind + assert_equal 'keys(b)', key_index.target + + value_index = @cluster.keyspace('simplex').table('multi_index_test').index('value_index') + assert_equal 'value_index', value_index.name + assert_equal 'multi_index_test', value_index.table.name + assert_equal :composites, value_index.kind + assert_equal 'values(b)', value_index.target + end + +end diff --git a/integration/indexes/materialized_view_test.rb b/integration/indexes/materialized_view_test.rb new file mode 100644 index 000000000..c8f316cb6 --- /dev/null +++ b/integration/indexes/materialized_view_test.rb @@ -0,0 +1,293 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require File.dirname(__FILE__) + '/../integration_test_case.rb' + +class MaterializedViewTest < IntegrationTestCase + + def setup + return if CCM.cassandra_version < '3.0.0' + + @@ccm_cluster.setup_schema("CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}") + + @cluster = Cassandra.cluster( + schema_refresh_delay: 0.1, + schema_refresh_timeout: 0.1 + ) + @listener = SchemaChangeListener.new(@cluster) + @session = @cluster.connect('simplex') + + @session.execute("CREATE TABLE simplex.scores( + user TEXT, + game TEXT, + year INT, + month INT, + day INT, + score INT, + PRIMARY KEY (user, game, year, month, day) + )") + @session.execute("CREATE MATERIALIZED VIEW simplex.monthlyhigh AS + SELECT game, year, month, score, user, day FROM simplex.scores + WHERE game IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND day IS NOT NULL + PRIMARY KEY ((game, year, month), score, user, day) + WITH CLUSTERING ORDER BY (score DESC, user ASC, day ASC)") + @listener.wait_for_materialized_view('simplex', 'monthlyhigh') + end + + def teardown + @cluster && @cluster.close + end + + # Test for retrieving materialized view metadata + # + # test_can_retrieve_materialized_view_metadata tests that all pieces of materialized view metadata can be retrieved. + # It goes through each piece of materialized view metadata and verifies that each piece is as expected. + # + # @since 3.0.0 + # @jira_ticket RUBY-167 + # @expected_result materialized view metadata should be retrieved. + # + # @test_category materialized_view + # + def test_can_retrieve_materialized_view_metadata + skip("Materialized views were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + assert @cluster.keyspace('simplex').has_materialized_view?('monthlyhigh') + mv_meta = @cluster.keyspace('simplex').materialized_view('monthlyhigh') + + table = @cluster.keyspace('simplex').table('scores') + assert table.has_materialized_view?('monthlyhigh') + assert_same(mv_meta, table.materialized_view('monthlyhigh')) + + assert_equal 'monthlyhigh', mv_meta.name + refute_nil mv_meta.id + assert_equal 'scores', mv_meta.base_table.name + assert_equal 'simplex', mv_meta.keyspace.name + refute_nil mv_meta.options + + assert_columns([['game', :text], ['year', :int], ['month', :int], ['score', :int], ['user', :text], ['day', :int]], + mv_meta.primary_key) + assert_columns([['game', :text], ['year', :int], ['month', :int]], mv_meta.partition_key) + assert_columns([['score', :int], ['user', :text], ['day', :int]], mv_meta.clustering_columns) + + assert_equal 6, mv_meta.columns.size + mv_meta.each_column do |column| + assert ['game', 'year', 'month', 'score', 'user', 'day'].any? { |name| name == column.name } + assert [:text, :int].any? { |type| type == column.type.kind } + end + end + + # Test for retrieving mv metadata with quoted identifiers + # + # test_can_retrieve_quoted_mv_metadata tests that all pieces of materialized view metadata can be retrieved, when the + # view has quoted identifiers. It goes through each piece of materialized view metadata and verifies that each piece + # is as expected. + # + # @since 3.0.0 + # @jira_ticket RUBY-167 + # @expected_result mv metadata with quoted identifiers should be retrieved. + # + # @test_category materialized_view + # + def test_can_retrieve_quoted_mv_metadata + skip("Materialized views were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + @session.execute("CREATE TABLE simplex.test ( + \"theKey\" int, + \"the;Clustering\" int, + \"the Value\" int, + PRIMARY KEY (\"theKey\", \"the;Clustering\"))") + @session.execute("CREATE MATERIALIZED VIEW simplex.mv1 AS + SELECT \"theKey\", \"the;Clustering\", \"the Value\" + FROM simplex.test + WHERE \"theKey\" IS NOT NULL AND \"the;Clustering\" IS NOT NULL AND \"the Value\" IS NOT NULL + PRIMARY KEY (\"theKey\", \"the;Clustering\")") + + @listener.wait_for_materialized_view('simplex', 'mv1') + + assert @cluster.keyspace('simplex').has_materialized_view?('mv1') + mv_meta = @cluster.keyspace('simplex').materialized_view('mv1') + + assert_equal 'mv1', mv_meta.name + refute_nil mv_meta.id + assert_equal 'test', mv_meta.base_table.name + assert_equal 'simplex', mv_meta.keyspace.name + refute_nil mv_meta.options + + assert_columns([['theKey', :int], ['the;Clustering', :int]], mv_meta.primary_key) + assert_columns([['theKey', :int]], mv_meta.partition_key) + assert_columns([['the;Clustering', :int]], mv_meta.clustering_columns) + + assert_equal 3, mv_meta.columns.size + mv_meta.each_column do |column| + assert ['theKey', 'the;Clustering', 'the Value'].any? { |name| name == column.name } + assert_equal :int, column.type.kind + end + end + + # Test for column ordering in materialized view metadata + # + # test_column_ordering_is_deterministic tests that the metadata relating to the columns are retrieved in the proper + # order. This proper order is: partition key, clustering columns, and then all other columns alphanumerically. + # + # @since 3.0.0 + # @jira_ticket RUBY-167 + # @expected_result column ordering should be correct in materialized view metadata. + # + # @test_category materialized_view + # + def test_column_ordering_is_deterministic + skip("Materialized views were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + assert @cluster.keyspace('simplex').has_materialized_view?('monthlyhigh') + mv_cql = Regexp.new(/CREATE MATERIALIZED VIEW simplex.monthlyhigh AS +SELECT game, year, month, score, "user", day +FROM simplex.scores +WHERE game IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND day IS NOT NULL +PRIMARY KEY \(\(game, year, month\), score, "user", day\) +WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE';/) + + mv_meta = @cluster.keyspace('simplex').materialized_view('monthlyhigh') + assert_match mv_cql, mv_meta.to_cql + + col_names = ['game', 'year', 'month', 'score', 'user', 'day'] + mv_meta.each_column do |column| + assert_equal col_names[0], column.name + col_names.delete_at(0) + end + end + + # Test for retrieving mv metadata updates + # + # test_materialized_view_metadata_updates tests that materialized view metadata is updated when there is any update + # to the materialized view. It first creates a simple materialized view and verifies that the default compaction + # strategy is SizeTieredCompactionStrategy. It then alters the compaction strategy to LeveledCompactionStrategy and + # verifies that the metadata is properly updated. + # + # @since 3.0.0 + # @jira_ticket RUBY-167 + # @expected_result mv metadata should be updated when the mv is updated. + # + # @test_category materialized_view + # + def test_materialized_view_metadata_updates + skip("Materialized views were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + @session.execute("CREATE TABLE simplex.test (pk int PRIMARY KEY, c int)") + @session.execute("CREATE MATERIALIZED VIEW simplex.mv1 AS SELECT c FROM simplex.test WHERE c IS NOT NULL PRIMARY KEY (pk, c)") + + @listener.wait_for_materialized_view('simplex', 'mv1') + + assert @cluster.keyspace('simplex').has_materialized_view?('mv1') + mv_meta = @cluster.keyspace('simplex').materialized_view('mv1') + assert_equal 'SizeTieredCompactionStrategy', mv_meta.options.compaction_strategy.class_name + + @session.execute("ALTER MATERIALIZED VIEW simplex.mv1 WITH compaction = { 'class' : 'LeveledCompactionStrategy' }") + @cluster.refresh_schema + mv_meta = @cluster.keyspace('simplex').materialized_view('mv1') + assert_equal 'LeveledCompactionStrategy', mv_meta.options.compaction_strategy.class_name + end + + # Test for retrieving mv metadata drops + # + # test_materialized_view_metadata_drop tests that materialized view metadata is removed when the materialized view + # is dropped. It first creates a simple materialized view and verifies that its metadata can be accessed. It then + # drops this materialized view and verifies that the metadata for the view no longer exists. + # + # @since 3.0.0 + # @jira_ticket RUBY-167 + # @expected_result mv metadata should be removed when the mv is dropped. + # + # @test_category materialized_view + # + def test_materialized_view_metadata_drop + skip("Materialized views were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + @session.execute("CREATE TABLE simplex.test (pk int PRIMARY KEY, c int)") + @session.execute("CREATE MATERIALIZED VIEW simplex.mv1 AS SELECT c FROM simplex.test WHERE c IS NOT NULL PRIMARY KEY (pk, c)") + + @listener.wait_for_materialized_view('simplex', 'mv1') + assert @cluster.keyspace('simplex').has_materialized_view?('mv1') + + @session.execute("DROP MATERIALIZED VIEW simplex.mv1") + @cluster.refresh_schema + refute @cluster.keyspace('simplex').has_materialized_view?('mv1') + end + + # Test for retrieving mv metadata updates from base table changes + # + # test_base_table_column_addition tests that materialized view metadata is properly updated when there is a change to + # the underlying base table. It first creates a basic table and a materialized view. It then alters the table to add + # a new column, verifying that the alteration is in the table's metadata. Finally, it verifies that the alteration + # has been propagated to the materialized view's metadata as well. + # + # @since 3.0.0 + # @jira_ticket RUBY-167 + # @expected_result mv metadata should be updated when the base table is updated. + # + # @test_category materialized_view + # + def test_base_table_column_addition + skip("Materialized views were introduced in Cassandra 3.0.0") if CCM.cassandra_version < '3.0.0' + + @session.execute("CREATE TABLE simplex.scores2( + user TEXT, + game TEXT, + year INT, + month INT, + day INT, + score INT, + PRIMARY KEY (user, game, year, month, day) + )") + @session.execute("CREATE MATERIALIZED VIEW simplex.alltimehigh AS + SELECT * FROM simplex.scores2 + WHERE game IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND day IS NOT NULL + PRIMARY KEY (game, score, user, year, month, day) + WITH CLUSTERING ORDER BY (score DESC)") + + @listener.wait_for_materialized_view('simplex', 'alltimehigh') + + assert @cluster.keyspace('simplex').has_materialized_view?('alltimehigh') + mv_meta = @cluster.keyspace('simplex').materialized_view('alltimehigh') + refute mv_meta.has_column?('fouls') + + @session.execute("ALTER TABLE simplex.scores2 ADD fouls INT") + @cluster.refresh_schema + + table_meta = @cluster.keyspace('simplex').table('scores2') + assert table_meta.has_column?('fouls') + assert_equal :int, table_meta.column('fouls').type.kind + + mv_meta = @cluster.keyspace('simplex').materialized_view('alltimehigh') + assert mv_meta.has_column?('fouls') + assert_equal :int, mv_meta.column('fouls').type.kind + end +end diff --git a/integration/integration_test_case.rb b/integration/integration_test_case.rb index b8b1737b3..fe5aa4e1a 100644 --- a/integration/integration_test_case.rb +++ b/integration/integration_test_case.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ require File.dirname(__FILE__) + '/../support/ccm.rb' require File.dirname(__FILE__) + '/../support/retry.rb' +require File.dirname(__FILE__) + '/schema_change_listener.rb' require 'minitest/unit' require 'minitest/autorun' require 'cassandra' @@ -28,7 +29,7 @@ class IntegrationTestCase < MiniTest::Unit::TestCase @@ccm_cluster = nil def self.before_suite - @@ccm_cluster = CCM.setup_cluster(1, 1) + @@ccm_cluster = CCM.setup_cluster(1, 1) unless self == IntegrationTestCase end def self.after_suite @@ -37,6 +38,15 @@ def self.after_suite def before_setup puts ANSI::Code.magenta("\n===== Begin #{self.__name__} ====") end + + def assert_columns(expected_names_and_types, actual_columns) + assert_equal(expected_names_and_types.size, actual_columns.size) + + expected_names_and_types.zip(actual_columns) do |expected, actual_column| + assert_equal expected[0], actual_column.name + assert_equal expected[1], actual_column.type.kind + end + end end class IntegrationUnit < MiniTest::Unit diff --git a/integration/load_balancing/round_robin_test.rb b/integration/load_balancing/round_robin_test.rb index 275e1bb4c..9da97ea7a 100644 --- a/integration/load_balancing/round_robin_test.rb +++ b/integration/load_balancing/round_robin_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,27 +34,26 @@ def setup_schema # Test for basic round robin load balancing policy # - # test_round_robin_used_explicitly tests the round robin policy by explicitly using it - # during the cluster connection. 4 queries are made, and each query should go to a different - # node in the Cassandra cluster. - # - # @return [String] List of hosts used in the query execution. + # test_round_robin_used_explicitly tests the round robin policy by explicitly using it during the cluster connection. + # 4 queries are made, and each query should go to a different node in the Cassandra cluster. Since each query goes + # to a different Cassandra node, this test also verifies that the round robin policy ignores datacenters. # # @since 1.0.0 - # @expected_result Each of the 4 queries should be routed to a different node in the cluster. + # @expected_result Each of the 4 queries should be routed to a different node in the cluster, across datacenters. # - # @test_assumptions Existing Cassandra cluster with keyspace 'simplex' and table 'users'. + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. # @test_category load_balancing:round_robin # - def test_round_robin_used_explicitly + def test_round_robin_used_explicitly_and_ignores_datacenters setup_schema policy = Cassandra::LoadBalancing::Policies::RoundRobin.new - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') hosts_used = [] 4.times do info = session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info + assert_equal 1, info.hosts.size hosts_used.push(info.hosts.last.ip.to_s) end @@ -62,13 +61,24 @@ def test_round_robin_used_explicitly cluster.close end + # Test for whitelisted round robin load balancing policy + # + # test_whitelisted_round_robin tests the round robin policy using a whitelist. 4 queries are made, and each query + # should go to a whitelisted node in the Cassandra cluster. + # + # @since 1.0.0 + # @expected_result Each of the 4 queries should be routed to a whitelisted node in the cluster. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # def test_whitelisted_round_robin setup_schema - allowed_ips = ["127.0.0.1", "127.0.0.3"] + allowed_ips = ['127.0.0.1', '127.0.0.3'] round_robin = Cassandra::LoadBalancing::Policies::RoundRobin.new whitelist = Cassandra::LoadBalancing::Policies::WhiteList.new(allowed_ips, round_robin) - cluster = Cassandra.cluster(load_balancing_policy: whitelist, consistency: :one) - session = cluster.connect("simplex") + cluster = Cassandra.cluster(load_balancing_policy: whitelist) + session = cluster.connect('simplex') hosts_used = [] 4.times do @@ -81,29 +91,24 @@ def test_whitelisted_round_robin cluster.close end - def test_round_robin_ignores_datacenters - setup_schema - policy = Cassandra::LoadBalancing::Policies::RoundRobin.new - cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) - session = cluster.connect("simplex") - - hosts_used = [] - 4.times do - info = session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info - assert_equal 1, info.hosts.size - hosts_used.push(info.hosts.last.ip.to_s) - end - - assert_equal ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], hosts_used.sort - cluster.close - end - + # Test for basic dc-aware round robin load balancing policy + # + # test_dc_aware_round_robin_queries_to_local_dc tests the round robin policy wrapping around a datacenter-aware + # policy. Using dc2 (node3, node4), 4 queries are made, and each query should go to a one of these two nodes in the + # Cassandra cluster. Note the default consistency is :local_one. + # + # @since 1.0.0 + # @expected_result Each of the 4 queries should be routed to a node in the local dc, dc2 (node3 or node4). + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # def test_dc_aware_round_robin_queries_to_local_dc setup_schema - datacenter = "dc1" + datacenter = 'dc2' policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) - session = cluster.connect("simplex") + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') hosts_used = [] 4.times do @@ -111,61 +116,100 @@ def test_dc_aware_round_robin_queries_to_local_dc hosts_used.push(info.hosts.last.ip.to_s) end - assert_equal ['127.0.0.1', '127.0.0.2'], hosts_used.sort.uniq + assert_equal ['127.0.0.3', '127.0.0.4'], hosts_used.sort.uniq cluster.close end - def test_dc_aware_round_robin_queries_to_remote_dc_if_local_down + # Test for dc-aware round robin load balancing policy with local down + # + # test_dc_aware_round_robin_does_not_query_to_remote_dc_by_default_if_local_down tests the dc-aware round robin + # policy's behavior when the local dc is down. It performs a query and verifies that a NoHostsAvailable error is + # raised as no local nodes are available. Note the default consistency is :local_one. + # + # @expected_errors [Cassandra::Errors::NoHostsAvailable] When local dc is down. + # + # @since 1.0.0 + # @expected_result A NoHostsAvailable should be raised as the local dc is down, and no remotes are specified. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # + def test_dc_aware_round_robin_does_not_query_to_remote_dc_by_default_if_local_down setup_schema - datacenter = "dc2" + datacenter = 'dc1' policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') - @@ccm_cluster.stop_node('node3') - @@ccm_cluster.stop_node('node4') + @@ccm_cluster.stop_node('node1') + @@ccm_cluster.stop_node('node2') - hosts_used = [] - 4.times do - info = session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info - hosts_used.push(info.hosts.last.ip.to_s) + assert_raises(Cassandra::Errors::NoHostsAvailable) do + session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info end - assert_equal ['127.0.0.1', '127.0.0.2'], hosts_used.sort.uniq cluster.close end - def test_raise_error_on_dc_aware_round_robin_unable_to_query_to_required_dc + # Test for dc-aware round robin lbp with local down but remotes enabled + # + # test_dc_aware_round_robin_can_query_to_remote_dc_if_local_down tests the dc-aware round robin policy's behavior + # when the local dc is down, but remote hosts are enabled. It first specifies the load balancing policy with + # max_remote_hosts_to_use = nil to allow unlimited remote hosts to be used for non-local consistency queries. It then + # performs 4 queries and verifies that hosts from dc2 (node3, node4) are used. Note the consistency is :one to allow + # for remote hosts to be used for the query. + # + # @since 1.0.0 + # @expected_result Each of the 4 queries should be routed a different node in the remote dc, dc2 (node3 or node4). + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # + def test_dc_aware_round_robin_can_query_to_remote_dc_if_local_down setup_schema - datacenter = "dc1" - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") + datacenter = 'dc1' + max_remote_hosts_to_use = nil + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) + session = cluster.connect('simplex') @@ccm_cluster.stop_node('node1') @@ccm_cluster.stop_node('node2') - begin - Retry.with_attempts(5, Cassandra::Errors::WriteTimeoutError) { - session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)") } - rescue Cassandra::Errors::NoHostsAvailable => e - raise e unless e.errors.first.last.is_a?(Cassandra::Errors::UnavailableError) + hosts_used = [] + 4.times do + info = session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info + hosts_used.push(info.hosts.last.ip.to_s) end - @@ccm_cluster.start_node('node1') + assert_equal ['127.0.0.3', '127.0.0.4'], hosts_used.sort.uniq cluster.close end + # Test for dc-aware round robin lbp with local down but one remote enabled + # + # test_dc_aware_round_robin_routes_up_to_max_hosts_in_remote tests the dc-aware round robin policy's behavior + # when the local dc is down, but one remote host is enabled. It first specifies the load balancing policy with + # max_remote_hosts_to_use = 1 to allow up to 1 remote host to be used for non-local consistency queries. It then + # performs 4 queries and verifies that one host from dc2 (node3, node4) is used. Note the consistency is :one to allow + # for remote hosts to be used for the query. + # + # @since 1.0.0 + # @expected_result Each of the 4 queries should be routed one node in the remote dc, dc2 (node3 or node4). + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # def test_dc_aware_round_robin_routes_up_to_max_hosts_in_remote setup_schema - datacenter = "dc2" - remotes_to_try = 1 - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, remotes_to_try) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") + datacenter = 'dc1' + max_remote_hosts_to_use = 1 + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) + session = cluster.connect('simplex') - @@ccm_cluster.stop_node('node3') - @@ccm_cluster.stop_node('node4') + @@ccm_cluster.stop_node('node1') + @@ccm_cluster.stop_node('node2') hosts_used = [] 4.times do @@ -174,50 +218,111 @@ def test_dc_aware_round_robin_routes_up_to_max_hosts_in_remote end assert_equal 1, hosts_used.uniq.size - refute_includes(hosts_used, "127.0.0.3") - refute_includes(hosts_used, "127.0.0.4") + assert_includes(['127.0.0.3', '127.0.0.4'], hosts_used.uniq.first) cluster.close end - def test_raise_error_on_dc_aware_round_robin_unable_to_route_local_consistencies_to_remote + # Test for dc-aware round robin lbp with local down but remotes disabled for local consistencies + # + # test_dc_aware_round_robin_does_not_query_to_remote_dc_for_local_consistency_by_default_if_local_down tests the + # dc-aware round robin policy's behavior for local consistency queries when the local dc is down, but only remote + # hosts are enabled. It first specifies the load balancing policy with max_remote_hosts_to_use = 2 to allow + # up to 2 remote hosts to be used for non-local consistency queries. It then performs a query and verifies that a + # NoHostsAvailable error is raised, as no local nodes are available for the local consistency query. Note the default + # consistency is :local_one. + # + # @expected_errors [Cassandra::Errors::NoHostsAvailable] When local dc is down, local consistency is not enabled for remotes. + # + # @since 3.0.0 + # @jira_ticket RUBY-211 + # @expected_result A NoHostsAvailable should be raised as the local dc is down, and no remotes are specified for local consistency. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # + def test_dc_aware_round_robin_does_not_query_to_remote_dc_for_local_consistency_by_default_if_local_down setup_schema - datacenter = "dc2" - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") + datacenter = 'dc1' + max_remote_hosts_to_use = 2 + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') - @@ccm_cluster.stop_node('node3') - @@ccm_cluster.stop_node('node4') + @@ccm_cluster.stop_node('node1') + @@ccm_cluster.stop_node('node2') - assert_raises(Cassandra::Errors::NoHostsAvailable) do - session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)", :consistency => :local_one) + assert_raises(Cassandra::Errors::NoHostsAvailable) do + session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info end cluster.close end - def test_dc_aware_round_robin_can_route_local_consistencies_to_remote + # Test for dc-aware round robin lbp with local down but remotes enabled for local consistencies + # + # test_dc_aware_round_robin_can_query_to_remote_dc_for_local_if_local_down tests the dc-aware round robin policy's + # behavior when the local dc is down, but remote hosts are enabled with local consistencies. It first specifies the + # load balancing policy with max_remote_hosts_to_use = 2, and use_remote_hosts_for_local_consistency = true, to allow + # up to 2 remote hosts to be used for local consistency queries. It then performs 4 queries and verifies that hosts + # from dc2 (node3, node4) are used. Note the default consistency is :local_one. + # + # @since 1.0.0 + # @expected_result Each of the 4 local-consistency queries should be routed a different node in the remote dc, dc2. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # + def test_dc_aware_round_robin_can_query_to_remote_dc_for_local_consistency_if_local_down setup_schema - datacenter = "dc2" - use_remote = true - policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, nil, use_remote) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") + datacenter = 'dc1' + max_remote_hosts_to_use = 2 + use_remote_hosts_for_local_consistency = true + policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use, + use_remote_hosts_for_local_consistency) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') - @@ccm_cluster.stop_node('node3') - @@ccm_cluster.stop_node('node4') + @@ccm_cluster.stop_node('node1') + @@ccm_cluster.stop_node('node2') hosts_used = [] 4.times do - info = session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)", - :consistency => :local_one).execution_info + info = session.execute("INSERT INTO users (user_id, first, last, age) VALUES (0, 'John', 'Doe', 40)").execution_info hosts_used.push(info.hosts.last.ip.to_s) end - assert_equal ['127.0.0.1', '127.0.0.2'], hosts_used.sort.uniq + assert_equal ['127.0.0.3', '127.0.0.4'], hosts_used.sort.uniq cluster.close end + # Test for dc-aware round robin lbp with local down but zero remotes enabled for local consistencies + # + # test_dc_aware_round_robin_cannot_query_to_remote_dc_for_local_consistency_if_max_hosts_zero tests the dc-aware + # round robin policy's behavior when the local dc is down, but zero remote hosts are enabled with local consistencies. + # It attempts to create a load balancing policy with max_remote_hosts_to_use = 0, + # and use_remote_hosts_for_local_consistency = true, and verifies than an ArgumentError is raised. + # + # @expected_errors [ArgumentError] When zero max_remote_hosts_to_use is specified with use_remote_hosts_for_local_consistency. + # + # @since 3.0.0 + # @jira_ticket RUBY-211 + # @expected_result An ArgumentError should be raised. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:round_robin + # + def test_dc_aware_round_robin_cannot_query_to_remote_dc_for_local_consistency_if_max_hosts_zero + setup_schema + datacenter = 'dc1' + max_remote_hosts_to_use = 0 + use_remote_hosts_for_local_consistency = true + + assert_raises(ArgumentError) do + Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use, + use_remote_hosts_for_local_consistency) + end + end + # Test for TryNextHost retry decision # # test_try_next_host_retry_decision tests that TryNextHost retry decision is being properly used when returned from the @@ -228,6 +333,7 @@ def test_dc_aware_round_robin_can_route_local_consistencies_to_remote # @expected_errors [Cassandra::Errors::NoHostsAvailable] When all hosts are down. # # @since 3.0.0 + # @jira_ticket RUBY-104 # @expected_result Each of the queries should be fulfilled by an available host. # # @test_assumptions Existing Cassandra cluster with keyspace 'simplex' and table 'users'. diff --git a/integration/load_balancing/token_aware_test.rb b/integration/load_balancing/token_aware_test.rb index b10ae486a..e6e6a4443 100644 --- a/integration/load_balancing/token_aware_test.rb +++ b/integration/load_balancing/token_aware_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ def test_token_aware_datacenter_aware_is_used_by_default cluster.close end - def test_token_aware_routes_to_primary_replica + def test_token_aware_routes_to_primary_replica_in_primary_dc setup_schema base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc1') policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) @@ -85,11 +85,46 @@ def test_token_aware_routes_to_primary_replica cluster.close end - def test_token_aware_routes_to_next_replica_if_primary_down + # Test for token-aware load balancing policy with primary dc down + # + # test_token_aware_does_not_by_default_route_to_next_replica_if_primary_dc_down tests the token aware policy's + # behavior when the primary dc is down. It performs a query and verifies that a NoHostsAvailable error is + # raised as no primary replicas are available. Note the default consistency is :local_one. + # + # @expected_errors [Cassandra::Errors::NoHostsAvailable] When primary dc is down. + # + # @since 1.0.0 + # @expected_result A NoHostsAvailable should be raised as the primary dc is down, and no remotes are specified. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:token_aware + # + def test_token_aware_does_not_by_default_route_to_next_replica_if_primary_dc_down setup_schema - base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc1') + datacenter = 'dc1' + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) + policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') + + select = Retry.with_attempts(5) { session.prepare("SELECT token(user_id) FROM simplex.users WHERE user_id = ?") } + + @@ccm_cluster.stop_node("node1") + @@ccm_cluster.stop_node("node2") + + assert_raises(Cassandra::Errors::NoHostsAvailable) do + Retry.with_attempts(5) { session.execute(select, arguments: [2]) } + end + + cluster.close + end + + def test_token_aware_can_route_to_next_replica_if_primary_dc_down + setup_schema + max_remote_hosts_to_use = nil + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc1', max_remote_hosts_to_use) policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) session = cluster.connect("simplex") Retry.with_attempts(5, Cassandra::Errors::InvalidError) do @@ -109,7 +144,7 @@ def test_token_aware_routes_to_next_replica_if_primary_down result = Retry.with_attempts(5) { session.execute(select, arguments: [2]) } assert_equal 1, result.execution_info.hosts.size - assert ["127.0.0.3", "127.0.0.4"].include?(result.execution_info.hosts.last.ip.to_s) + assert_includes(["127.0.0.3", "127.0.0.4"], result.execution_info.hosts.last.ip.to_s) end cluster.close @@ -121,7 +156,7 @@ def test_token_aware_routes_to_next_whitelisted_replica_if_primary_down round_robin = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new('dc1') whitelist = Cassandra::LoadBalancing::Policies::WhiteList.new(allowed_ips, round_robin) policy = Cassandra::LoadBalancing::Policies::TokenAware.new(whitelist) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) + cluster = Cassandra.cluster(load_balancing_policy: policy, consistency: :one) session = cluster.connect("simplex") Retry.with_attempts(5, Cassandra::Errors::InvalidError) do @@ -135,68 +170,78 @@ def test_token_aware_routes_to_next_whitelisted_replica_if_primary_down cluster.close end - def test_token_aware_routes_to_primary_replica_in_primary_dc + # Test for token-aware lbp with primary down but remotes disabled for local consistencies + # + # test_token_aware_does_not_route_to_next_replica_for_local_consistency_by_default_if_primary_dc_down tests the + # token-aware policy's behavior for local consistency queries when the primary dc is down, but only remote + # hosts are enabled. It first specifies the load balancing policy with max_remote_hosts_to_use = nil to allow + # unlimited remote hosts to be used for non-local consistency queries. It then performs a query and verifies that a + # NoHostsAvailable error is raised, as no primary replicas are available for the local consistency query. Note the + # default consistency is :local_one. + # + # @expected_errors [Cassandra::Errors::NoHostsAvailable] When primary dc is down, local consistency is not enabled for remotes. + # + # @since 3.0.0 + # @jira_ticket RUBY-211 + # @expected_result A NoHostsAvailable should be raised as the primary dc is down, and no remotes are specified for local consistency. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:token_aware + # + def test_token_aware_does_not_route_to_next_replica_for_local_consistency_by_default_if_primary_dc_down setup_schema - datacenter = "dc2" - base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) + datacenter = 'dc1' + max_remote_hosts_to_use = nil + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use) policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) cluster = Cassandra.cluster(load_balancing_policy: policy) - session = cluster.connect("simplex") - - Retry.with_attempts(5, Cassandra::Errors::InvalidError) do - select = Retry.with_attempts(5) { session.prepare("SELECT token(user_id) FROM simplex.users WHERE user_id = ?") } - - result = Retry.with_attempts(5) { session.execute(select, arguments: [0]) } - assert_equal 2945182322382062539, result.first.values.first - assert_equal "127.0.0.3", result.execution_info.hosts.last.ip.to_s + session = cluster.connect('simplex') - result = Retry.with_attempts(5) { session.execute(select, arguments: [1]) } - assert_equal 6292367497774912474, result.first.values.first - assert_equal "127.0.0.3", result.execution_info.hosts.last.ip.to_s + select = Retry.with_attempts(5) { session.prepare("SELECT token(user_id) FROM simplex.users WHERE user_id = ?") } - result = Retry.with_attempts(5) { session.execute(select, arguments: [2]) } - assert_equal -8218881827949364593, result.first.values.first - assert_equal "127.0.0.4", result.execution_info.hosts.last.ip.to_s + @@ccm_cluster.stop_node("node1") + @@ccm_cluster.stop_node("node2") - result = Retry.with_attempts(5) { session.execute(select, arguments: [3]) } - assert_equal -8048510690352527683, result.first.values.first - assert_equal "127.0.0.4", result.execution_info.hosts.last.ip.to_s + assert_raises(Cassandra::Errors::NoHostsAvailable) do + Retry.with_attempts(5) { session.execute(select, arguments: [2]) } end cluster.close end - def test_token_aware_routes_to_secondary_replica_if_primary_dc_down + # Test for token-aware lbp with primary down but remotes enabled for local consistencies + # + # test_token_aware_can_route_to_next_replica_for_local_consistency_if_primary_dc_down tests the token-aware policy's + # behavior when the primary dc is down, but remote replicas are enabled with local consistencies. It first specifies the + # load balancing policy with max_remote_hosts_to_use = nil, and use_remote_hosts_for_local_consistency = true, to allow + # unlimited remote hosts to be used for local consistency queries. It then performs 4 queries and verifies that replicas + # from dc2 (node3, node4) are used. Note the default consistency is :local_one. + # + # @since 1.0.0 + # @expected_result Each of the 4 local-consistency queries should be routed a different replica in the remote dc, dc2. + # + # @test_assumptions Existing Cassandra cluster with two datacenters, keyspace 'simplex', and table 'users'. + # @test_category load_balancing:token_aware + # + def test_token_aware_can_route_to_next_replica_for_local_consistency_if_primary_dc_down setup_schema - datacenter = "dc2" - base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter) + datacenter = 'dc1' + max_remote_hosts_to_use = nil + use_remote_hosts_for_local_consistency = true + base_policy = Cassandra::LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use, + use_remote_hosts_for_local_consistency) policy = Cassandra::LoadBalancing::Policies::TokenAware.new(base_policy) - cluster = Cassandra.cluster(consistency: :one, load_balancing_policy: policy) - session = cluster.connect("simplex") - - Retry.with_attempts(5, Cassandra::Errors::InvalidError) do - select = Retry.with_attempts(5) { session.prepare("SELECT token(user_id) FROM simplex.users WHERE user_id = ?") } - - result = Retry.with_attempts(5) { session.execute(select, arguments: [2]) } - assert_equal 1, result.execution_info.hosts.size - assert_equal "127.0.0.4", result.execution_info.hosts.last.ip.to_s - - @@ccm_cluster.stop_node("node4") + cluster = Cassandra.cluster(load_balancing_policy: policy) + session = cluster.connect('simplex') - result = Retry.with_attempts(5) { session.execute(select, arguments: [2]) } - assert_equal 1, result.execution_info.hosts.size - assert_equal "127.0.0.3", result.execution_info.hosts.last.ip.to_s + select = Retry.with_attempts(5) { session.prepare("SELECT token(user_id) FROM simplex.users WHERE user_id = ?") } - @@ccm_cluster.stop_node("node3") + @@ccm_cluster.stop_node("node1") + @@ccm_cluster.stop_node("node2") - result = Retry.with_attempts(5) { session.execute(select, arguments: [2], consistency: :one) } - assert_equal 1, result.execution_info.hosts.size - assert_equal "127.0.0.2", result.execution_info.hosts.last.ip.to_s - - result = Retry.with_attempts(5) { session.execute(select, arguments: [2], consistency: :one) } - assert_equal 1, result.execution_info.hosts.size - assert_equal "127.0.0.1", result.execution_info.hosts.last.ip.to_s - end + result = Retry.with_attempts(5) { session.execute(select, arguments: [2]) } + assert_equal 1, result.execution_info.hosts.size + assert_includes(["127.0.0.3", "127.0.0.4"], result.execution_info.hosts.last.ip.to_s) cluster.close end diff --git a/integration/load_balancing/try_next_host_retry_policy.rb b/integration/load_balancing/try_next_host_retry_policy.rb index 597972ecb..8abe9f4c4 100644 --- a/integration/load_balancing/try_next_host_retry_policy.rb +++ b/integration/load_balancing/try_next_host_retry_policy.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/integration/metadata_test.rb b/integration/metadata_test.rb new file mode 100644 index 000000000..19b855cb9 --- /dev/null +++ b/integration/metadata_test.rb @@ -0,0 +1,412 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require File.dirname(__FILE__) + '/integration_test_case.rb' +require File.dirname(__FILE__) + '/datatype_utils.rb' + +class MetadataTest < IntegrationTestCase + + def setup + @@ccm_cluster.setup_schema("CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}") + + @cluster = Cassandra.cluster( + schema_refresh_delay: 0.1, + schema_refresh_timeout: 0.1 + ) + @listener = SchemaChangeListener.new(@cluster) + @session = @cluster.connect('simplex') + @session.execute("CREATE TABLE simplex.users (user_id bigint, first text, last text, age int, PRIMARY KEY (user_id, last))") + @listener.wait_for_table('simplex', 'users') + @session.execute("CREATE TABLE simplex.blobby (key blob PRIMARY KEY, f1 blob, f2 blob) WITH COMPACT STORAGE") + @listener.wait_for_table('simplex', 'blobby') + @session.execute("CREATE TABLE simplex.dense (f1 int, f2 int, f3 int, PRIMARY KEY (f1, f2)) WITH COMPACT STORAGE") + @listener.wait_for_table('simplex', 'dense') + @session.execute("CREATE TABLE simplex.custom (f1 int PRIMARY KEY," \ + " f2 'org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UUIDType,org.apache.cassandra.db.marshal.UTF8Type)')") + @listener.wait_for_table('simplex', 'custom') + @session.execute("CREATE TABLE simplex.test1 (key text, value text, PRIMARY KEY(key))") + @listener.wait_for_table('simplex', 'test1') + @session.execute("CREATE TABLE simplex.test2 (key text, value text, PRIMARY KEY(key))") + @listener.wait_for_table('simplex', 'test2') + @session.execute("CREATE TABLE simplex.audit (key timeuuid, keyspace_name text, table_name text, primary_key text, PRIMARY KEY(key))") + @listener.wait_for_table('simplex', 'audit') + @session.execute(<" column is excluded from table metadata + # for dense tables. + # + # @since 3.0.0 + # @jira_ticket RUBY-185 + # @expected_result the metadata should only report columns we've consciously added to the table. + # + # @test_category metadata + # + def test_skip_internal_columns_for_dense_table + # NOTE: It seems that the dense table does not have an empty-type column. The Java driver has logic to + # handle that, but maybe it's outdated and unnecessary. This test serves the purpose of keeping us on guard, + # in case some version of C* does create the internal column; if we encounter such a C*, the test will fail and + # we'll go to the effort of fixing the issue. + + assert @cluster.keyspace('simplex').has_table?('dense') + table_meta = @cluster.keyspace('simplex').table('dense') + table_cql = Regexp.new(/CREATE TABLE simplex\.dense \( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY \("f1", "f2"\) +\) +WITH COMPACT STORAGE/) + + assert_equal 0, table_meta.to_cql =~ table_cql, "actual cql: #{table_meta.to_cql}" + assert_equal 3, table_meta.columns.size + + table_meta.each_column do |column| + assert ['f1', 'f2', 'f3'].any? { |name| name == column.name } + assert_equal :int, column.type.kind + refute column.static? + end + end + + # Test for handling custom type columns in table metadata + # + # test_custom_type_column_in_table tests that a custom type column in a table is processed properly + # when collecting table metadata. + # + # @since 3.0.0 + # @jira_ticket RUBY-186 + # @expected_result the metadata should correctly report the custom type column. + # + # @test_category metadata + # + def test_custom_type_column_in_table + skip("Custom type representation was changed in Cassandra 3.0 to be a single-quoted string") if CCM.cassandra_version < '3.0.0' + + assert @cluster.keyspace('simplex').has_table?('custom') + table_meta = @cluster.keyspace('simplex').table('custom') + table_cql = Regexp.new(/CREATE TABLE simplex\."custom" \( + "f1" int PRIMARY KEY, + "f2" 'org.apache.cassandra.db.marshal.CompositeType\(org.apache.cassandra.db.marshal.UUIDType,org.apache.cassandra.db.marshal.UTF8Type\)' +\)/) + + assert_equal 0, table_meta.to_cql =~ table_cql, "actual cql: #{table_meta.to_cql}" + assert_equal 2, table_meta.columns.size + column = table_meta.columns[0] + assert_equal 'f1', column.name + assert_equal :int, column.type.kind + column = table_meta.columns[1] + assert_equal 'f2', column.name + assert_equal :custom, column.type.kind + assert_equal 'org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UUIDType,org.apache.cassandra.db.marshal.UTF8Type)', + column.type.name + end + + # Test for retrieving crc_check_balance property + # + # test_table_metadata_contains_crc_check_balance tests that the 'crc_check_balance' property of table metadata is able + # to be retrieved. + # + # @since 3.0.0 + # @jira_ticket RUBY-179 + # @expected_result crc_check_balance property should be retrieved from the table metadata + # + # @test_category metadata + # + def test_table_metadata_contains_crc_check_balance + skip("The crc_check_balance property on a table was introduced in Cassandra 3.0") if CCM.cassandra_version < '3.0.0' + + assert @cluster.keyspace('simplex').has_table?('users') + table_meta = @cluster.keyspace('simplex').table('users') + assert_equal 1.0, table_meta.options.crc_check_chance + end + + # Test for retrieving extensions property + # + # test_table_metadata_contains_extensions tests that the 'extensions' property of table metadata is able to be + # retrieved. + # + # @since 3.0.0 + # @jira_ticket RUBY-170 + # @expected_result extensions property should be retrieved from the table metadata + # + # @test_category metadata + # + def test_table_metadata_contains_extensions + skip("The extensions property on a table was introduced in Cassandra 3.0") if CCM.cassandra_version < '3.0.0' + + assert @cluster.keyspace('simplex').has_table?('users') + table_meta = @cluster.keyspace('simplex').table('users') + assert_empty table_meta.options.extensions + end + + # Test for retrieving trigger metadata + # + # test_can_retrieve_trigger_metadata tests that all pieces of trigger metadata can be retrieved. It first creates a + # simple trigger. It then goes through each piece of the trigger metadata and verifies that each piece is as expected. + # It finally creates another trigger with the same name, on a different table and verifies that it is retrieved and + # complete. + # + # @since 3.1.0 + # @jira_ticket RUBY-187 + # @expected_result trigger metadata should be retrieved. + # + # @test_category metadata + # + def test_can_retrieve_trigger_metadata + skip("Triggers were introduced in Cassandra 2.0") if CCM.cassandra_version < '2.0.0' + + # trigger1, on test1 table + @session.execute("CREATE TRIGGER trigger1 ON simplex.test1 USING 'org.apache.cassandra.triggers.AuditTrigger'") + @listener.wait_for_trigger('simplex', 'test1', 'trigger1') + + assert @cluster.keyspace('simplex').table('test1').has_trigger?('trigger1') + trigger_meta = @cluster.keyspace('simplex').table('test1').trigger('trigger1') + assert_equal 'trigger1', trigger_meta.name + assert_equal 'test1', trigger_meta.table.name + assert_equal 'org.apache.cassandra.triggers.AuditTrigger', trigger_meta.options['class'] + + # trigger1, on test2 table + @session.execute("CREATE TRIGGER trigger1 ON simplex.test2 USING 'org.apache.cassandra.triggers.AuditTrigger'") + @listener.wait_for_trigger('simplex', 'test2', 'trigger1') + + assert @cluster.keyspace('simplex').table('test2').has_trigger?('trigger1') + trigger_meta2 = @cluster.keyspace('simplex').table('test2').trigger('trigger1') + assert_equal 'trigger1', trigger_meta2.name + assert_equal 'test2', trigger_meta2.table.name + assert_equal 'org.apache.cassandra.triggers.AuditTrigger', trigger_meta2.options['class'] + + refute_equal trigger_meta, trigger_meta2 + end +end diff --git a/integration/functions/schema_change_listener.rb b/integration/schema_change_listener.rb similarity index 68% rename from integration/functions/schema_change_listener.rb rename to integration/schema_change_listener.rb index f03d5a925..76b1b7f5f 100644 --- a/integration/functions/schema_change_listener.rb +++ b/integration/schema_change_listener.rb @@ -54,6 +54,32 @@ def wait_for_aggregate(keyspace_name, aggregate_name, *args) end end + def wait_for_table(keyspace_name, table_name, *args) + wait_for_change(keyspace_name, 2) do |ks| + ks.has_table?(table_name, *args) + end + end + + def wait_for_index(keyspace_name, table_name, index_name, *args) + self.wait_for_table(keyspace_name, table_name) + wait_for_change(keyspace_name, 2) do |ks| + ks.table(table_name).has_index?(index_name, *args) + end + end + + def wait_for_materialized_view(keyspace_name, view_name, *args) + wait_for_change(keyspace_name, 2) do |ks| + ks.has_materialized_view?(view_name, *args) + end + end + + def wait_for_trigger(keyspace_name, table_name, trigger_name, *args) + self.wait_for_table(keyspace_name, table_name) + wait_for_change(keyspace_name, 2) do |ks| + ks.table(table_name).has_trigger?(trigger_name, *args) + end + end + def keyspace_changed(keyspace) # This looks a little strange, but here's the idea: if we don't have # Condition's for this keyspace, immediately return. Otherwise, for each @@ -61,4 +87,5 @@ def keyspace_changed(keyspace) @conditions.fetch(keyspace.name) { return }.each { |c| c.evaluate(keyspace) } nil end -end \ No newline at end of file +end + diff --git a/integration/security/authentication_test.rb b/integration/security/authentication_test.rb index b769c8c49..9b335281b 100644 --- a/integration/security/authentication_test.rb +++ b/integration/security/authentication_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,8 +25,7 @@ def self.before_suite end def self.after_suite - @@ccm_cluster && @@ccm_cluster.disable_authentication - super + CCM.remove_cluster(@@ccm_cluster.name) end # Test for basic successful authentication @@ -54,7 +53,7 @@ def test_can_authenticate_to_cluster refute_nil cluster ensure - cluster.close + cluster && cluster.close end # Test for basic unsuccessful authentication diff --git a/integration/security/ssl_authenticated_encryption_test.rb b/integration/security/ssl_authenticated_encryption_test.rb index ea678dbd2..61185d75e 100644 --- a/integration/security/ssl_authenticated_encryption_test.rb +++ b/integration/security/ssl_authenticated_encryption_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,8 +25,7 @@ def self.before_suite end def self.after_suite - @@ccm_cluster && @@ccm_cluster.disable_ssl - super + CCM.remove_cluster(@@ccm_cluster.name) end def test_can_connect_with_ssl_authentication @@ -38,7 +37,7 @@ def test_can_connect_with_ssl_authentication ) refute_nil cluster ensure - cluster.close + cluster && cluster.close end def test_raise_error_on_invalid_ssl_auth diff --git a/integration/security/ssl_encryption_test.rb b/integration/security/ssl_encryption_test.rb index f84fc8434..daea13ed8 100644 --- a/integration/security/ssl_encryption_test.rb +++ b/integration/security/ssl_encryption_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,15 +25,14 @@ def self.before_suite end def self.after_suite - @@ccm_cluster && @@ccm_cluster.disable_ssl - super + CCM.remove_cluster(@@ccm_cluster.name) end def test_can_connect_with_default_ssl cluster = Cassandra.cluster(ssl: true) refute_nil cluster ensure - cluster.close + cluster && cluster.close end def test_raise_error_when_not_using_ssl @@ -47,7 +46,7 @@ def test_can_connect_with_ssl_ca cluster = Cassandra.cluster(server_cert: @@server_cert) refute_nil cluster ensure - cluster.close + cluster && cluster.close end def test_raise_error_on_invalid_ca_provided diff --git a/integration/serial_consistency_test.rb b/integration/serial_consistency_test.rb index 4366f6db3..0bc7c6ecf 100644 --- a/integration/serial_consistency_test.rb +++ b/integration/serial_consistency_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/integration/session_test.rb b/integration/session_test.rb index 430991b2c..6892148b2 100644 --- a/integration/session_test.rb +++ b/integration/session_test.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -580,7 +580,7 @@ def test_client_side_timestamps_with_past_timestamp setup_schema begin - cluster = Cassandra.cluster(client_timestamps: true) + cluster = Cassandra.cluster(client_timestamps: :simple) session = cluster.connect("simplex") # Insert in the present @@ -637,11 +637,11 @@ def test_raise_error_on_future_resolution_timeout @@ccm_cluster.block_node("node1") future = session.execute_async("SELECT * FROM users") - start_time = Time.now.to_i + start_time = Time.now assert_raises(Cassandra::Errors::TimeoutError) do future.get(2) end - assert_equal 2, Time.now.to_i - start_time + assert_in_delta(2, Time.now - start_time, 0.5) ensure @@ccm_cluster.unblock_nodes cluster && cluster.close @@ -751,4 +751,280 @@ def test_unset_values cluster && cluster.close end + # Test for host broadcast_address and listen_address + # + # test_host_broadcast_listen_address tests that broadcast_address and listen_address metadata are able to be retrieved + # from a host, when they are available. + # + # @since 3.0.0 + # @jira_ticket RUBY-168 + # @expected_result broadcast_address and listen_address should be retrieved + # + # @test_category host:metadata + # + def test_host_broadcast_listen_address + cluster = Cassandra.cluster + cluster.hosts.each do |host| + assert_equal '127.0.0.1', host.broadcast_address.to_s + assert_equal '127.0.0.1', host.listen_address.to_s + end + ensure + cluster && cluster.close + end + + # Test for setting the protocol_version + # + # test_can_set_protocol_version_explicitly tests that the protocol_version for a cluster object can be set, such that + # this version is used explicitly when connecting to the Cassandra cluster, rather than automatically negotiating it. + # It first tests that a valid protocol version can be set. It then tests that setting a protocol version lower than + # the Cassandra cluster supports throws a Cassandra::Errors::NoHostsAvailable, or ArgumentError if invalid. Finally, + # it tests that setting a protocol version higher than the Cassandra cluster supports throws a + # Cassandra::Errors::ProtocolError, or ArgumentError if invalid. + # + # @expected_errors [ArgumentError] When an invalid protocol version (< 1 or > 4) is specified. + # @expected_errors [Cassandra::Errors::NoHostsAvailable] When a protocol version lower than supported is specified. + # @expected_errors [Cassandra::Errors::ProtocolError] When a protocol version higher than supported is specified. + # + # @since 3.0.0 + # @jira_ticket RUBY-165 + # @expected_result protocol versions specified should be honored when connecting to the Cassandra cluster. + # + # @test_category connection + # + def test_can_set_protocol_version_explicitly + ## Setting a valid protocol version + if CCM.cassandra_version < '3.0.0' + cluster = Cassandra.cluster(protocol_version: 1) + assert_equal 1, cluster.instance_variable_get(:@connection_options).protocol_version + else + cluster = Cassandra.cluster(protocol_version: 3) + assert_equal 3, cluster.instance_variable_get(:@connection_options).protocol_version + end + + ## Setting protocol version lower than supported + # Protocol version 0 is invalid + assert_raises(ArgumentError) do + Cassandra.cluster(protocol_version: 0) + end + + # C* 3.0+ does not support protocol version < 3 + if CCM.cassandra_version >= '3.0.0' + assert_raises(Cassandra::Errors::ProtocolError) do + Cassandra.cluster(protocol_version: 2) + end + end + + ## Setting protocol version higher than supported + # Protocol version > 4 is invalid + assert_raises(ArgumentError) do + Cassandra.cluster(protocol_version: 10) + end + + # C* < 2.2 does not support protocol version 4 + if CCM.cassandra_version < '2.2.0' + assert_raises(Cassandra::Errors::ProtocolError) do + Cassandra.cluster(protocol_version: 4) + end + end + ensure + cluster && cluster.close + end + + # Test for cluster and session object inspection + # + # test_cluster_session_inspect tests that cluster and session objects can be inspected, and their stored options can + # be retrieved in the inspect string. It first creates a simple cluster and session object with some options defined. + # It then inspects these objects and verifies that the options set are visible in the inspect string. + # + # @since 3.0.0 + # @jira_ticket RUBY-162 + # @expected_result cluster and session options should be visible in the inspect string + # + # @test_category connection + # + def test_cluster_session_inspect + setup_schema + + cluster = Cassandra.cluster(hosts: ['127.0.0.1'], consistency: :quorum, page_size: 10) + session = cluster.connect('simplex') + + cluster_inspect = cluster.inspect + assert_match(/name="ruby-driver-.*"/, cluster_inspect) + assert_match(/hosts=\[.* @ip=127\.0\.0\.1>\]/, cluster_inspect) + assert_match(/port=9042/, cluster_inspect) + assert_match(/protocol_version=[1-4]/, cluster_inspect) + assert_match(/keyspaces=\[.* @name=simplex>\]/, cluster_inspect) + assert_match(/load_balancing_policy=#)') + + # RUBY-291 - We can't connect to the cluster when a UDT name contains capital letters (and is thus quoted) + cluster2 = Cassandra.cluster + session2 = cluster2.connect("simplex") + + # Test non-prepared statement + session2.execute("INSERT INTO \"MyTable\" (a, b) VALUES (0, {age: 30, name: 'John', gender: 'male'})") + user_value = session2.execute('SELECT b FROM "MyTable" where a=0').first['b'] + assert_equal 30, user_value.age + assert_equal 'John', user_value.name + assert_equal 'male', user_value.gender + + # Test prepared statement + insert = Retry.with_attempts(5) { session2.prepare("INSERT INTO \"MyTable\" (a, b) VALUES (?, ?)") } + Retry.with_attempts(5) { session2.execute(insert, arguments: [1, Cassandra::UDT.new(age: 25, name: 'Jane', gender: 'female')]) } + + user_value = session2.execute('SELECT b FROM "MyTable" where a=1').first['b'] + assert_equal 25, user_value.age + assert_equal 'Jane', user_value.name + assert_equal 'female', user_value.gender + ensure + cluster && cluster.close + cluster2 && cluster2.close + end + def test_can_insert_same_udt_different_keyspaces skip("UDTs are only available in C* after 2.1") if CCM.cassandra_version < '2.1.0' diff --git a/lib/cassandra.rb b/lib/cassandra.rb index a4ab7c641..f85c76e83 100644 --- a/lib/cassandra.rb +++ b/lib/cassandra.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,8 +34,10 @@ module Cassandra # A list of all supported request consistencies - # @see http://www.datastax.com/documentation/cassandra/2.0/cassandra/dml/dml_config_consistency_c.html Consistency levels in Apache Cassandra 2.0 - # @see http://www.datastax.com/documentation/cassandra/1.2/cassandra/dml/dml_config_consistency_c.html Consistency levels in Apache Cassandra 1.2 + # @see http://www.datastax.com/documentation/cassandra/2.0/cassandra/dml/dml_config_consistency_c.html Consistency + # levels in Apache Cassandra 2.0 + # @see http://www.datastax.com/documentation/cassandra/1.2/cassandra/dml/dml_config_consistency_c.html Consistency + # levels in Apache Cassandra 1.2 # @see Cassandra::Session#execute_async CONSISTENCIES = [:any, :one, :two, :three, :quorum, :all, :local_quorum, :each_quorum, :serial, :local_serial, :local_one].freeze @@ -47,9 +49,55 @@ module Cassandra # A list of all possible write types that a # {Cassandra::Errors::WriteTimeoutError} can have. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v2.spec#L872-L887 Description of possible types of writes in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v2.spec#L872-L887 Description of + # possible types of writes in Apache Cassandra native protocol spec v1 WRITE_TYPES = [:simple, :batch, :unlogged_batch, :counter, :batch_log].freeze + CLUSTER_OPTIONS = [ + :address_resolution, + :address_resolution_policy, + :allow_beta_protocol, + :auth_provider, + :client_cert, + :client_timestamps, + :compression, + :compressor, + :connect_timeout, + :connections_per_local_node, + :connections_per_remote_node, + :consistency, + :credentials, + :custom_types, + :datacenter, + :execution_profiles, + :futures_factory, + :heartbeat_interval, + :hosts, + :idle_timeout, + :listeners, + :load_balancing_policy, + :logger, + :nodelay, + :reconnection_policy, + :retry_policy, + :page_size, + :passphrase, + :password, + :port, + :private_key, + :protocol_version, + :requests_per_connection, + :schema_refresh_delay, + :schema_refresh_timeout, + :server_cert, + :shuffle_replicas, + :ssl, + :synchronize_schema, + :timeout, + :trace, + :username + ].freeze + # Creates a {Cassandra::Cluster Cluster instance}. # # @option options [Array] :hosts (['127.0.0.1']) a list of @@ -71,10 +119,14 @@ module Cassandra # found by the default Token-Aware Load Balancing Policy should be # shuffled. See {Cassandra::LoadBalancing::Policies::TokenAware#initialize Token-Aware Load Balancing Policy}. # + # @option options [Hash] :execution_profiles (nil) + # Hash of {Cassandra::Execution::Profile}s that are available for client use (e.g. + # {Session#execute}, {Session#execute_async}, {Session#prepare}, and {Session#prepare_async}). + # # @option options [Numeric] :connect_timeout (10) connection timeout in # seconds. Setting value to `nil` will reset it to 5 seconds. # - # @option options [Numeric] :timeout (10) request execution timeout in + # @option options [Numeric] :timeout (12) request execution timeout in # seconds. Setting value to `nil` will remove request timeout. # # @option options [Numeric] :heartbeat_interval (30) how often should a @@ -141,11 +193,21 @@ module Cassandra # for nodes that use the v3 or later protocol, and `128` for nodes that use the # v2 or earlier protocol. # - # @option options [Boolean] :client_timestamps (false) whether the driver - # should send timestamps for each executed statement. Enabling this setting - # allows mitigating Cassandra cluster clock skew because the timestamp of - # the client machine will be used. This does not help mitigate application - # cluster clock skew. + # @option options [Integer] :protocol_version (nil) Version of protocol to speak to + # nodes. By default, this is auto-negotiated to the highest common protocol version + # that all nodes in `:hosts` speak. + # + # @option options [Boolean] :allow_beta_protocol (false) whether the driver should attempt to speak to nodes + # with a beta version of the newest protocol (which is still under development). USE WITH CAUTION! + # + # @option options [Boolean, Cassandra::TimestampGenerator] :client_timestamps (false) whether the driver + # should send timestamps for each executed statement and possibly which timestamp generator to use. Enabling this + # setting helps mitigate Cassandra cluster clock skew because the timestamp of the client machine will be used. + # This does not help mitigate application cluster clock skew. Also accepts an initialized + # {Cassandra::TimestampGenerator}, `:simple` (indicating an instance of {Cassandra::TimestampGenerator::Simple}), + # or `:monotonic` (indicating an instance of {Cassandra::TimestampGenerator::TickingOnDuplicate}). If set to true, + # it defaults to {Cassandra::TimestampGenerator::Simple} for all Ruby flavors except JRuby. On JRuby, it defaults to + # {Cassandra::TimestampGenerator::TickingOnDuplicate}. # # @option options [Boolean] :synchronize_schema (true) whether the driver # should automatically keep schema metadata synchronized. When enabled, the @@ -241,28 +303,7 @@ def self.cluster(options = {}) # @return [Cassandra::Future] a future resolving to the # cluster instance. def self.cluster_async(options = {}) - options = validate_and_massage_options(options) - hosts = [] - - Array(options.fetch(:hosts, '127.0.0.1')).each do |host| - case host - when ::IPAddr - hosts << host - when ::String # ip address or hostname - Resolv.each_address(host) do |ip| - hosts << ::IPAddr.new(ip) - end - else - raise ::ArgumentError, ":hosts must be String or IPAddr, #{host.inspect} given" - end - end - - if hosts.empty? - raise ::ArgumentError, - ":hosts #{options[:hosts].inspect} could not be resolved to any ip address" - end - - hosts.shuffle! + options, hosts = validate_and_massage_options(options) rescue => e futures = options.fetch(:futures_factory) { return Future::Error.new(e) } futures.error(e) @@ -274,49 +315,19 @@ def self.cluster_async(options = {}) # @private SSL_CLASSES = [::TrueClass, ::FalseClass, ::OpenSSL::SSL::SSLContext].freeze + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity # @private def self.validate_and_massage_options(options) options = options.select do |key, _| - [ - :address_resolution, - :address_resolution_policy, - :auth_provider, - :client_cert, - :client_timestamps, - :compression, - :compressor, - :connect_timeout, - :connections_per_local_node, - :connections_per_remote_node, - :consistency, - :credentials, - :datacenter, - :futures_factory, - :heartbeat_interval, - :hosts, - :idle_timeout, - :listeners, - :load_balancing_policy, - :logger, - :nodelay, - :reconnection_policy, - :retry_policy, - :page_size, - :passphrase, - :password, - :port, - :private_key, - :requests_per_connection, - :schema_refresh_delay, - :schema_refresh_timeout, - :server_cert, - :shuffle_replicas, - :ssl, - :synchronize_schema, - :timeout, - :trace, - :username - ].include?(key) + CLUSTER_OPTIONS.include?(key) + end + + if options.key?(:execution_profiles) + [:load_balancing_policy, :retry_policy, :timeout, :consistency].each do |opt| + raise ::ArgumentError, "#{opt} is not allowed when execution profiles are used" if options.key?(opt) + end end has_username = options.key?(:username) @@ -507,15 +518,9 @@ def self.validate_and_massage_options(options) end end - if options.key?(:timeout) - timeout = options[:timeout] - - unless timeout.nil? - Util.assert_instance_of(::Numeric, timeout) do - ":timeout must be a number of seconds, #{timeout.inspect} given" - end - Util.assert(timeout > 0) { ":timeout must be greater than 0, #{timeout} given" } - end + if options.key?(:execution_profiles) + Util.assert_instance_of(::Hash, options[:execution_profiles], + ':execution_profiles must be a hash of entries.') end if options.key?(:heartbeat_interval) @@ -566,17 +571,6 @@ def self.validate_and_massage_options(options) end end - if options.key?(:load_balancing_policy) - load_balancing_policy = options[:load_balancing_policy] - methods = [:host_up, :host_down, :host_found, :host_lost, :setup, :teardown, - :distance, :plan] - - Util.assert_responds_to_all(methods, load_balancing_policy) do - ":load_balancing_policy #{load_balancing_policy.inspect} must respond " \ - "to #{methods.inspect}, but doesn't" - end - end - if options.key?(:reconnection_policy) reconnection_policy = options[:reconnection_policy] @@ -586,34 +580,15 @@ def self.validate_and_massage_options(options) end end - if options.key?(:retry_policy) - retry_policy = options[:retry_policy] - methods = [:read_timeout, :write_timeout, :unavailable] - - Util.assert_responds_to_all(methods, retry_policy) do - ":retry_policy #{retry_policy.inspect} must respond to #{methods.inspect}, " \ - "but doesn't" - end - end + # Validate options that go in an execution profile. Instantiating one + # causes validation automatically. + Cassandra::Execution::Profile.new(options) options[:listeners] = Array(options[:listeners]) if options.key?(:listeners) - - if options.key?(:consistency) - consistency = options[:consistency] - - Util.assert_one_of(CONSISTENCIES, consistency) do - ":consistency must be one of #{CONSISTENCIES.inspect}, " \ - "#{consistency.inspect} given" - end - end - options[:nodelay] = !!options[:nodelay] if options.key?(:nodelay) - options[:trace] = !!options[:trace] if options.key?(:trace) - - if options.key?(:shuffle_replicas) - options[:shuffle_replicas] = !!options[:shuffle_replicas] - end + options[:shuffle_replicas] = !!options[:shuffle_replicas] if options.key?(:shuffle_replicas) + options[:allow_beta_protocol] = !!options[:allow_beta_protocol] if options.key?(:allow_beta_protocol) if options.key?(:page_size) page_size = options[:page_size] @@ -627,6 +602,19 @@ def self.validate_and_massage_options(options) end end + if options.key?(:protocol_version) + protocol_version = options[:protocol_version] + unless protocol_version.nil? + Util.assert_instance_of(::Integer, protocol_version) + Util.assert_one_of(1..Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION, protocol_version, + ':protocol_version must be a positive integer between 1 and ' \ + "#{Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION}, #{protocol_version.inspect} given") + end + end + + Util.assert(!(options[:allow_beta_protocol] && options[:protocol_version]), + 'only one of :allow_beta_protocol and :protocol_version may be specified, both given') + if options.key?(:futures_factory) futures_factory = options[:futures_factory] methods = [:error, :value, :promise, :all] @@ -642,14 +630,14 @@ def self.validate_and_massage_options(options) case address_resolution when :none - # do nothing + # do nothing when :ec2_multi_region options[:address_resolution_policy] = AddressResolution::Policies::EC2MultiRegion.new else raise ::ArgumentError, ':address_resolution must be either :none or :ec2_multi_region, ' \ - "#{address_resolution.inspect} given" + "#{address_resolution.inspect} given" end end @@ -662,12 +650,35 @@ def self.validate_and_massage_options(options) end end - if options.key?(:synchronize_schema) - options[:synchronize_schema] = !!options[:synchronize_schema] - end + options[:synchronize_schema] = !!options[:synchronize_schema] if options.key?(:synchronize_schema) if options.key?(:client_timestamps) - options[:client_timestamps] = !!options[:client_timestamps] + timestamp_generator = case options[:client_timestamps] + when true + if RUBY_ENGINE == 'jruby' + Cassandra::TimestampGenerator::TickingOnDuplicate.new + else + Cassandra::TimestampGenerator::Simple.new + end + when false + nil + when :simple + Cassandra::TimestampGenerator::Simple.new + when :monotonic + Cassandra::TimestampGenerator::TickingOnDuplicate.new + else + # The value must be a generator instance. + options[:client_timestamps] + end + + if timestamp_generator + Util.assert_responds_to(:next, timestamp_generator) do + ":client_timestamps #{options[:client_timestamps].inspect} must be a boolean, :simple, :monotonic, or " \ + 'an object that responds to :next' + end + end + options.delete(:client_timestamps) + options[:timestamp_generator] = timestamp_generator end if options.key?(:connections_per_local_node) @@ -712,7 +723,31 @@ def self.validate_and_massage_options(options) end end end - options + + # Get host addresses. + hosts = [] + + Array(options.fetch(:hosts, '127.0.0.1')).each do |host| + case host + when ::IPAddr + hosts << host + when ::String # ip address or hostname + Resolv.each_address(host) do |ip| + hosts << ::IPAddr.new(ip) + end + else + raise ::ArgumentError, ":hosts must be String or IPAddr, #{host.inspect} given" + end + end + + if hosts.empty? + raise ::ArgumentError, + ":hosts #{options[:hosts].inspect} could not be resolved to any ip address" + end + + hosts.shuffle! + + [options, hosts] end # @private @@ -733,6 +768,8 @@ def self.validate_and_massage_options(options) DATE_OFFSET = (::Time.utc(1970, 1, 1).to_date.jd - 2**31) end +require 'cassandra/attr_boolean' +require 'cassandra/version' require 'cassandra/uuid' require 'cassandra/time_uuid' require 'cassandra/tuple' @@ -751,6 +788,7 @@ def self.validate_and_massage_options(options) require 'cassandra/executors' require 'cassandra/future' require 'cassandra/cluster' +require 'cassandra/custom_data' require 'cassandra/driver' require 'cassandra/host' require 'cassandra/session' @@ -763,28 +801,42 @@ def self.validate_and_massage_options(options) require 'cassandra/function' require 'cassandra/function_collection' require 'cassandra/column' +require 'cassandra/column_container' require 'cassandra/table' +require 'cassandra/materialized_view' require 'cassandra/keyspace' +require 'cassandra/index' +require 'cassandra/trigger' require 'cassandra/execution/info' require 'cassandra/execution/options' +require 'cassandra/execution/profile_manager' +require 'cassandra/execution/profile' require 'cassandra/execution/trace' require 'cassandra/load_balancing' require 'cassandra/reconnection' require 'cassandra/retry' require 'cassandra/address_resolution' +require 'cassandra/timestamp_generator' require 'cassandra/util' # murmur3 hash extension require 'cassandra_murmur3' +# SortedSet has a race condition where it does some class/global initialization when the first instance is created. +# If this is done in a multi-threaded environment, bad things can happen. So force the initialization here, +# when loading the C* module. +::SortedSet.new + module Cassandra # @private VOID_STATEMENT = Statements::Void.new # @private - VOID_OPTIONS = Execution::Options.new(consistency: :one) + VOID_OPTIONS = Execution::Options.new(consistency: :one, + load_balancing_policy: LoadBalancing::Policies::RoundRobin.new, + retry_policy: Retry::Policies::Default.new) # @private NO_HOSTS = Errors::NoHostsAvailable.new end diff --git a/lib/cassandra/address_resolution.rb b/lib/cassandra/address_resolution.rb index be3040a18..91a9d4f4a 100644 --- a/lib/cassandra/address_resolution.rb +++ b/lib/cassandra/address_resolution.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/address_resolution/policies/ec2_multi_region.rb b/lib/cassandra/address_resolution/policies/ec2_multi_region.rb index b3dab056f..a5eb0028c 100644 --- a/lib/cassandra/address_resolution/policies/ec2_multi_region.rb +++ b/lib/cassandra/address_resolution/policies/ec2_multi_region.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/address_resolution/policies/none.rb b/lib/cassandra/address_resolution/policies/none.rb index f6b8802c4..daca2bd03 100644 --- a/lib/cassandra/address_resolution/policies/none.rb +++ b/lib/cassandra/address_resolution/policies/none.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/aggregate.rb b/lib/cassandra/aggregate.rb index f607e1060..8491f7fcb 100644 --- a/lib/cassandra/aggregate.rb +++ b/lib/cassandra/aggregate.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/argument.rb b/lib/cassandra/argument.rb index fbc858f6b..5cbfd54ef 100644 --- a/lib/cassandra/argument.rb +++ b/lib/cassandra/argument.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/attr_boolean.rb b/lib/cassandra/attr_boolean.rb new file mode 100644 index 000000000..29ce86bf6 --- /dev/null +++ b/lib/cassandra/attr_boolean.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +# This file monkey-patches Module to have an attr_boolean method to make it easy +# for classes to define boolean instance variables with "foo?" reader methods. +# Inspired by http://stackoverflow.com/questions/4013591/attr-reader-with-question-mark-in-a-name +module Cassandra + module AttrBoolean + def attr_boolean(*names) + names.each do |name| + define_method(:"#{name}?") do + res = instance_variable_get(:"@#{name}") + !res.nil? && res != false + end + end + end + end +end diff --git a/lib/cassandra/auth.rb b/lib/cassandra/auth.rb index 6d40f6680..db995e58e 100644 --- a/lib/cassandra/auth.rb +++ b/lib/cassandra/auth.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ module Auth # # @see Cassandra::Auth::Providers class Provider - # @!method create_authenticator(authentication_class, protocol_version) + # @!method create_authenticator(authentication_class, host) # # Create a new authenticator object. This method will be called once per # connection that requires authentication. The auth provider can create @@ -45,6 +45,8 @@ class Provider # # @param authentication_class [String] the authentication class used by # the server. + # @param host [Cassandra::Host] the node to whom we're authenticating. + # # @return [Cassandra::Auth::Authenticator, nil] an object with an # interface matching {Cassandra::Auth::Authenticator} or `nil` if the # authentication class is not supported. @@ -59,7 +61,8 @@ class Provider # subclasses of this class, but need to implement the same methods. This # class exists only for documentation purposes. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v2.spec#L257-L273 Cassandra native protocol v2 SASL authentication + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v2.spec#L257-L273 Cassandra + # native protocol v2 SASL authentication # @see Cassandra::Auth::Provider#create_authenticator class Authenticator # @!method initial_response diff --git a/lib/cassandra/auth/providers.rb b/lib/cassandra/auth/providers.rb index 75cdcecff..9f12c8c0c 100644 --- a/lib/cassandra/auth/providers.rb +++ b/lib/cassandra/auth/providers.rb @@ -1,5 +1,5 @@ #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/auth/providers/password.rb b/lib/cassandra/auth/providers/password.rb index 0f2e91fa0..0d3e6a43f 100644 --- a/lib/cassandra/auth/providers/password.rb +++ b/lib/cassandra/auth/providers/password.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -53,24 +53,12 @@ def initialize(username, password) @password = password end - # Returns a Password Authenticator only if - # `org.apache.cassandra.auth.PasswordAuthenticator` is given. - # @param authentication_class [String] must equal to - # `org.apache.cassandra.auth.PasswordAuthenticator` - # @return [Cassandra::Auth::Authenticator] when `authentication_class == - # "org.apache.cassandra.auth.PasswordAuthenticator"` - # @return [nil] for all other values of `authentication_class` + # Returns a Password Authenticator + # @param authentication_class [String] ignored + # @return [Cassandra::Auth::Authenticator] def create_authenticator(authentication_class) - if authentication_class == PASSWORD_AUTHENTICATOR_FQCN - Authenticator.new(@username, @password) - end + Authenticator.new(@username, @password) end - - private - - # @private - PASSWORD_AUTHENTICATOR_FQCN = - 'org.apache.cassandra.auth.PasswordAuthenticator'.freeze end end end diff --git a/lib/cassandra/cassandra_logger.rb b/lib/cassandra/cassandra_logger.rb index c653493ce..1c9a0cc1a 100644 --- a/lib/cassandra/cassandra_logger.rb +++ b/lib/cassandra/cassandra_logger.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster.rb b/lib/cassandra/cluster.rb index 03a80a07e..923f0174b 100644 --- a/lib/cassandra/cluster.rb +++ b/lib/cassandra/cluster.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,12 +36,12 @@ def initialize(logger, cluster_metadata, execution_options, connection_options, - load_balancing_policy, + profile_manager, reconnection_policy, - retry_policy, address_resolution_policy, connector, - futures_factory) + futures_factory, + timestamp_generator) @logger = logger @io_reactor = io_reactor @executor = executor @@ -51,16 +51,16 @@ def initialize(logger, @metadata = cluster_metadata @execution_options = execution_options @connection_options = connection_options - @load_balancing_policy = load_balancing_policy + @profile_manager = profile_manager @reconnection_policy = reconnection_policy - @retry_policy = retry_policy @address_resolver = address_resolution_policy @connector = connector @futures = futures_factory + @timestamp_generator = timestamp_generator @control_connection.on_close do |_cause| begin - @load_balancing_policy.teardown(self) + @profile_manager.teardown(self) rescue nil end @@ -151,6 +151,40 @@ def each_keyspace(&block) # @return [Boolean] true or false def_delegators :@schema, :keyspace, :has_keyspace? + # @return [Integer] Cassandra native protocol port + def port + @connection_options.port + end + + # @return [Integer] the version of the native protocol used in communication with nodes + def protocol_version + @connection_options.protocol_version + end + + # @param name [String] Name of profile to retrieve + # @return [Cassandra::Execution::Profile] execution profile of the given name + def execution_profile(name) + @profile_manager.profiles[name] + end + + # Yield or enumerate each execution profile defined in this cluster + # @overload each_execution_profile + # @yieldparam name [String, Symbol] name of current profile + # @yieldparam profile [Cassandra::Execution::Profile] current profile + # @return [Cassandra::Cluster] self + # @overload each_execution_profile + # @return [Hash] a hash of profiles keyed on name + def each_execution_profile(&block) + if block_given? + @profile_manager.profiles.each_pair(&block) + self + else + # Return a dup of the hash to prevent the user from adding/removing profiles from the profile-manager. + @profile_manager.profiles.dup + end + end + alias execution_profiles each_execution_profile + # @!method refresh_schema_async # Trigger an asynchronous schema metadata refresh # @return [Cassandra::Future] a future that will be fulfilled when @@ -199,13 +233,13 @@ def connect_async(keyspace = nil) @schema, @io_reactor, @connector, - @load_balancing_policy, + @profile_manager, @reconnection_policy, - @retry_policy, @address_resolver, @connection_options, - @futures) - session = Session.new(client, @execution_options, @futures) + @futures, + @timestamp_generator) + session = Session.new(client, @execution_options, @futures, @profile_manager) promise = @futures.promise client.connect.on_complete do |f| @@ -275,7 +309,13 @@ def close # @private def inspect - "#<#{self.class.name}:0x#{object_id.to_s(16)}>" + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "name=#{name.inspect}, " \ + "port=#{@connection_options.port}, " \ + "protocol_version=#{@connection_options.protocol_version}, " \ + "execution_profiles=#{@profile_manager.profiles.inspect}, " \ + "hosts=#{hosts.inspect}, " \ + "keyspaces=#{keyspaces.inspect}>" end end end diff --git a/lib/cassandra/cluster/client.rb b/lib/cassandra/cluster/client.rb index 3251b0a53..670d3f6ba 100644 --- a/lib/cassandra/cluster/client.rb +++ b/lib/cassandra/cluster/client.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,29 +29,29 @@ def initialize(logger, cluster_schema, io_reactor, connector, - load_balancing_policy, + profile_manager, reconnection_policy, - retry_policy, address_resolution_policy, connection_options, - futures_factory) + futures_factory, + timestamp_generator) @logger = logger @registry = cluster_registry @schema = cluster_schema @reactor = io_reactor @connector = connector - @load_balancing_policy = load_balancing_policy + @profile_manager = profile_manager @reconnection_policy = reconnection_policy - @retry_policy = retry_policy @address_resolver = address_resolution_policy @connection_options = connection_options @futures = futures_factory @connections = ::Hash.new @prepared_statements = ::Hash.new - @preparing_statements = ::Hash.new + @preparing_statements = ::Hash.new {|hash, host| hash[host] = {}} @pending_connections = ::Hash.new @keyspace = nil @state = :idle + @timestamp_generator = timestamp_generator mon_initialize end @@ -65,7 +65,7 @@ def connect @state = :connecting @registry.each_host do |host| - distance = @load_balancing_policy.distance(host) + distance = @profile_manager.distance(host) case distance when :ignore @@ -83,7 +83,6 @@ def connect connecting_hosts[host] = pool_size @pending_connections[host] = 0 - @prepared_statements[host] = {} @preparing_statements[host] = {} @connections[host] = ConnectionPool.new end @@ -107,6 +106,16 @@ def connect failed_connections = connections.reject(&:connected?) + # convert Cassandra::Protocol::CqlProtocolHandler to something with a real host + failed_connections.map! do |c| + if c.host.is_a?(String) + host = @registry.each_host.detect { |h| h.ip.to_s == c.host } || raise("Unable to find host #{c.host}") + FailedConnection.new(c.error, host) + else + c + end + end + if failed_connections.size == connections.size errors = {} connections.each {|c| errors[c.host] = c.error unless c.error.nil?} @@ -166,7 +175,7 @@ def host_up(host) pool_size = 0 synchronize do - distance = @load_balancing_policy.distance(host) + distance = @profile_manager.distance(host) case distance when :ignore return Ione::Future.resolved @@ -182,7 +191,6 @@ def host_up(host) end @pending_connections[host] ||= 0 - @prepared_statements[host] = {} @preparing_statements[host] = {} @connections[host] = ConnectionPool.new end @@ -197,7 +205,6 @@ def host_down(host) return Ione::Future.resolved unless @connections.key?(host) @pending_connections.delete(host) unless @pending_connections[host] > 0 - @prepared_statements.delete(host) @preparing_statements.delete(host) pool = @connections.delete(host) end @@ -225,14 +232,12 @@ def query(statement, options) return @futures.error( Errors::ClientError.new( 'Positional arguments are not supported by the current version of ' \ - 'Apache Cassandra')) + 'Apache Cassandra' + ) + ) end - timestamp = nil - if @connection_options.client_timestamps? && - @connection_options.protocol_version > 2 - timestamp = ::Time.now - end + timestamp = @timestamp_generator.next if @timestamp_generator && @connection_options.protocol_version > 2 payload = nil payload = options.payload if @connection_options.protocol_version >= 4 request = Protocol::QueryRequest.new(statement.cql, @@ -250,7 +255,7 @@ def query(statement, options) promise = @futures.promise keyspace = @keyspace - plan = @load_balancing_policy.plan(keyspace, statement, options) + plan = options.load_balancing_policy.plan(keyspace, statement, options) send_request_by_plan(promise, keyspace, @@ -272,7 +277,7 @@ def prepare(cql, options) keyspace = @keyspace statement = VOID_STATEMENT - plan = @load_balancing_policy.plan(keyspace, statement, options) + plan = options.load_balancing_policy.plan(keyspace, statement, options) send_request_by_plan(promise, keyspace, @@ -286,11 +291,7 @@ def prepare(cql, options) end def execute(statement, options) - timestamp = nil - if @connection_options.client_timestamps? && - @connection_options.protocol_version > 2 - timestamp = ::Time.now - end + timestamp = @timestamp_generator.next if @timestamp_generator && @connection_options.protocol_version > 2 payload = nil payload = options.payload if @connection_options.protocol_version >= 4 timeout = options.timeout @@ -309,7 +310,7 @@ def execute(statement, options) promise = @futures.promise keyspace = @keyspace - plan = @load_balancing_policy.plan(keyspace, statement, options) + plan = options.load_balancing_policy.plan(keyspace, statement, options) execute_by_plan(promise, keyspace, statement, options, request, plan, timeout) @@ -321,14 +322,12 @@ def batch(statement, options) return @futures.error( Errors::ClientError.new( 'Batch statements are not supported by the current version of ' \ - 'Apache Cassandra')) + 'Apache Cassandra' + ) + ) end - timestamp = nil - if @connection_options.client_timestamps? && - @connection_options.protocol_version > 2 - timestamp = ::Time.now - end + timestamp = @timestamp_generator.next if @timestamp_generator && @connection_options.protocol_version > 2 payload = nil payload = options.payload if @connection_options.protocol_version >= 4 timeout = options.timeout @@ -339,7 +338,7 @@ def batch(statement, options) timestamp, payload) keyspace = @keyspace - plan = @load_balancing_policy.plan(keyspace, statement, options) + plan = options.load_balancing_policy.plan(keyspace, statement, options) promise = @futures.promise batch_by_plan(promise, keyspace, statement, options, request, plan, timeout) @@ -376,13 +375,15 @@ def inspect 'SELECT peer, rpc_address, schema_version FROM system.peers', EMPTY_LIST, EMPTY_LIST, - :one) + :one + ) SELECT_SCHEMA_LOCAL = Protocol::QueryRequest.new( "SELECT schema_version FROM system.local WHERE key='local'", EMPTY_LIST, EMPTY_LIST, - :one) + :one + ) def connected(f) if f.resolved? @@ -482,7 +483,7 @@ def connect_to_host(host, pool_size) @pending_connections[host] += size end - @logger.debug("Creating #{size} connections to #{host.ip}") + @logger.debug("Creating #{size} request connections to #{host.ip}") futures = size.times.map do @connector.connect(host).recover do |e| FailedConnection.new(e, host) @@ -501,7 +502,7 @@ def connect_to_host(host, pool_size) end end - @logger.debug("Created #{connections.size} connections to #{host.ip}") + @logger.debug("Created #{connections.size} request connections to #{host.ip}") pool = nil @@ -520,6 +521,12 @@ def connect_to_host(host, pool_size) connections.each do |connection| connection.on_closed do |cause| + if cause + @logger.info('Request connection closed ' \ + "(#{cause.class.name}: #{cause.message})") + else + @logger.info('Request connection closed') + end connect_to_host_maybe_retry(host, pool_size) if cause end end @@ -543,13 +550,16 @@ def execute_by_plan(promise, plan, timeout, errors = nil, - hosts = []) + hosts = [], + retries = -1) unless plan.has_next? promise.break(Errors::NoHostsAvailable.new(errors)) return end hosts << host = plan.next + retries += 1 + pool = nil synchronize { pool = @connections[host] } @@ -564,7 +574,8 @@ def execute_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end connection = pool.random_connection @@ -583,7 +594,8 @@ def execute_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) else s.on_failure do |e| if e.is_a?(Errors::HostError) || @@ -598,7 +610,8 @@ def execute_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -616,7 +629,8 @@ def execute_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end rescue => e errors ||= {} @@ -629,7 +643,8 @@ def execute_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end def prepare_and_send_request_by_plan(host, @@ -642,9 +657,16 @@ def prepare_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) cql = statement.cql - id = synchronize { @prepared_statements[host][cql] } + + # Get the prepared statement id for this statement from our cache if possible. We are optimistic + # that the statement has previously been prepared on all hosts, so the id will be valid. However, if + # we're in the midst of preparing the statement on the given host, we know that executing with the id + # will fail. So, act like we don't have the prepared-statement id in that case. + + id = synchronize { @preparing_statements[host][cql] ? nil : @prepared_statements[cql] } if id request.id = id @@ -658,7 +680,8 @@ def prepare_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) else prepare = prepare_statement(host, connection, cql, timeout) prepare.on_complete do |_| @@ -674,7 +697,8 @@ def prepare_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) else prepare.on_failure do |e| if e.is_a?(Errors::HostError) || @@ -689,7 +713,8 @@ def prepare_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -709,13 +734,15 @@ def batch_by_plan(promise, plan, timeout, errors = nil, - hosts = []) + hosts = [], + retries = -1) unless plan.has_next? promise.break(Errors::NoHostsAvailable.new(errors)) return end hosts << host = plan.next + retries += 1 pool = nil synchronize { pool = @connections[host] } @@ -730,7 +757,8 @@ def batch_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end connection = pool.random_connection @@ -749,7 +777,8 @@ def batch_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) else s.on_failure do |e| if e.is_a?(Errors::HostError) || @@ -764,7 +793,8 @@ def batch_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -782,7 +812,8 @@ def batch_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end rescue => e errors ||= {} @@ -795,7 +826,8 @@ def batch_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end def batch_and_send_request_by_plan(host, @@ -808,7 +840,8 @@ def batch_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) request.clear unprepared = Hash.new {|hash, cql| hash[cql] = []} @@ -816,7 +849,12 @@ def batch_and_send_request_by_plan(host, cql = statement.cql if statement.is_a?(Statements::Bound) - id = synchronize { @prepared_statements[host][cql] } + # Get the prepared statement id for this statement from our cache if possible. We are optimistic + # that the statement has previously been prepared on all hosts, so the id will be valid. However, if + # we're in the midst of preparing the statement on the given host, we know that executing with the id + # will fail. So, act like we don't have the prepared-statement id in that case. + + id = synchronize { @preparing_statements[host][cql] ? nil : @prepared_statements[cql] } if id request.add_prepared(id, statement.params, statement.params_types) @@ -839,7 +877,8 @@ def batch_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) else to_prepare = unprepared.to_a futures = to_prepare.map do |cql, _| @@ -867,7 +906,8 @@ def batch_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) else f.on_failure do |e| if e.is_a?(Errors::HostError) || @@ -882,7 +922,8 @@ def batch_and_send_request_by_plan(host, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -900,13 +941,15 @@ def send_request_by_plan(promise, plan, timeout, errors = nil, - hosts = []) + hosts = [], + retries = -1) unless plan.has_next? promise.break(Errors::NoHostsAvailable.new(errors)) return end hosts << host = plan.next + retries += 1 pool = nil synchronize { pool = @connections[host] } @@ -921,7 +964,8 @@ def send_request_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end connection = pool.random_connection @@ -940,7 +984,8 @@ def send_request_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) else s.on_failure do |e| if e.is_a?(Errors::HostError) || @@ -955,7 +1000,8 @@ def send_request_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -973,7 +1019,8 @@ def send_request_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end rescue => e errors ||= {} @@ -986,7 +1033,8 @@ def send_request_by_plan(promise, plan, timeout, errors, - hosts) + hosts, + retries) end def do_send_request_by_plan(host, @@ -1000,7 +1048,7 @@ def do_send_request_by_plan(host, timeout, errors, hosts, - retries = 0) + retries) request.retries = retries f = connection.send_request(request, timeout) @@ -1045,37 +1093,43 @@ def handle_response(response_future, case r when Protocol::UnavailableErrorResponse - decision = @retry_policy.unavailable(statement, - r.consistency, - r.required, - r.alive, - retries) + decision = options.retry_policy.unavailable(statement, + r.consistency, + r.required, + r.alive, + retries) when Protocol::WriteTimeoutErrorResponse - decision = @retry_policy.write_timeout(statement, - r.consistency, - r.write_type, - r.blockfor, - r.received, - retries) + decision = options.retry_policy.write_timeout(statement, + r.consistency, + r.write_type, + r.blockfor, + r.received, + retries) when Protocol::ReadTimeoutErrorResponse - decision = @retry_policy.read_timeout(statement, - r.consistency, - r.blockfor, - r.received, - r.data_present, - retries) + decision = options.retry_policy.read_timeout(statement, + r.consistency, + r.blockfor, + r.received, + r.data_present, + retries) when Protocol::UnpreparedErrorResponse - cql = statement.cql - - synchronize do - @preparing_statements[host].delete(cql) - @prepared_statements[host].delete(cql) + cql = nil + if statement.is_a?(Cassandra::Statements::Batch) + # Find the prepared statement with the prepared-statement-id reported by the node. + unprepared_child = statement.statements.select do |s| + (s.is_a?(Cassandra::Statements::Prepared) || s.is_a?(Cassandra::Statements::Bound)) && s.id == r.id + end.first + cql = unprepared_child ? unprepared_child.cql : nil + else + # This is a normal statement, so we have everything we need. + cql = statement.cql + synchronize { @preparing_statements[host].delete(cql) } end prepare = prepare_statement(host, connection, cql, timeout) prepare.on_complete do |_| if prepare.resolved? - request.id = prepare.value + request.id = prepare.value unless request.is_a?(Cassandra::Protocol::BatchRequest) do_send_request_by_plan(host, connection, promise, @@ -1086,7 +1140,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) else prepare.on_failure do |e| if e.is_a?(Errors::HostError) || @@ -1100,7 +1155,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -1129,7 +1185,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) when Protocol::ExecuteRequest execute_by_plan(promise, keyspace, @@ -1139,7 +1196,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) when Protocol::BatchRequest batch_by_plan(promise, keyspace, @@ -1149,28 +1207,29 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) end else promise.break(error) end when Protocol::SetKeyspaceResultResponse @keyspace = r.keyspace - promise.fulfill(Results::Void.new(r.custom_payload, - r.warnings, - r.trace_id, - keyspace, - statement, - options, - hosts, - request.consistency, - retries, - self, - @futures)) + promise.fulfill(Cassandra::Results::Void.new(r.custom_payload, + r.warnings, + r.trace_id, + keyspace, + statement, + options, + hosts, + request.consistency, + retries, + self, + @futures)) when Protocol::PreparedResultResponse cql = request.cql synchronize do - @prepared_statements[host][cql] = r.id + @prepared_statements[cql] = r.id @preparing_statements[host].delete(cql) end @@ -1179,7 +1238,8 @@ def handle_response(response_future, pk_idx ||= @schema.get_pk_idx(metadata) promise.fulfill( - Statements::Prepared.new(r.custom_payload, + Statements::Prepared.new(r.id, + r.custom_payload, r.warnings, cql, metadata, @@ -1193,7 +1253,8 @@ def handle_response(response_future, request.consistency, retries, self, - @connection_options)) + @connection_options) + ) when Protocol::RawRowsResultResponse r.materialize(statement.result_metadata) promise.fulfill( @@ -1209,7 +1270,8 @@ def handle_response(response_future, request.consistency, retries, self, - @futures)) + @futures) + ) when Protocol::RowsResultResponse promise.fulfill( Results::Paged.new(r.custom_payload, @@ -1224,7 +1286,8 @@ def handle_response(response_future, request.consistency, retries, self, - @futures)) + @futures) + ) when Protocol::SchemaChangeResultResponse if r.change == 'DROPPED' && r.target == Protocol::Constants::SCHEMA_CHANGE_TARGET_KEYSPACE @@ -1237,7 +1300,8 @@ def handle_response(response_future, unless f.resolved? f.on_failure do |e| @logger.error( - "Schema agreement failure (#{e.class.name}: #{e.message})") + "Schema agreement failure (#{e.class.name}: #{e.message})" + ) end end promise.fulfill( @@ -1251,7 +1315,8 @@ def handle_response(response_future, request.consistency, retries, self, - @futures)) + @futures) + ) end else promise.fulfill(Results::Void.new(r.custom_payload, @@ -1300,7 +1365,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) when Protocol::ExecuteRequest execute_by_plan(promise, keyspace, @@ -1310,7 +1376,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) when Protocol::BatchRequest batch_by_plan(promise, keyspace, @@ -1320,7 +1387,8 @@ def handle_response(response_future, plan, timeout, errors, - hosts) + hosts, + retries) else promise.break(e) end @@ -1328,7 +1396,7 @@ def handle_response(response_future, promise.fulfill( Results::Void.new(r.custom_payload, r.warnings, - r.trace_id, + nil, keyspace, statement, options, @@ -1336,7 +1404,8 @@ def handle_response(response_future, request.consistency, retries, self, - @futures)) + @futures) + ) when Retry::Decisions::Reraise promise.break( r.to_error(keyspace, @@ -1344,7 +1413,8 @@ def handle_response(response_future, options, hosts, request.consistency, - retries)) + retries) + ) else promise.break( r.to_error(keyspace, @@ -1352,7 +1422,8 @@ def handle_response(response_future, options, hosts, request.consistency, - retries)) + retries) + ) end end rescue => e @@ -1360,20 +1431,35 @@ def handle_response(response_future, end else response_future.on_failure do |ex| - errors[host] = ex - case request - when Protocol::QueryRequest, Protocol::PrepareRequest - send_request_by_plan(promise, - keyspace, - statement, - options, - request, - plan, - timeout, - errors, - hosts) - when Protocol::ExecuteRequest - execute_by_plan(promise, + if ex.is_a?(Errors::HostError) || + (ex.is_a?(Errors::TimeoutError) && statement.idempotent?) + + errors[host] = ex + case request + when Protocol::QueryRequest, Protocol::PrepareRequest + send_request_by_plan(promise, + keyspace, + statement, + options, + request, + plan, + timeout, + errors, + hosts, + retries) + when Protocol::ExecuteRequest + execute_by_plan(promise, + keyspace, + statement, + options, + request, + plan, + timeout, + errors, + hosts, + retries) + when Protocol::BatchRequest + batch_by_plan(promise, keyspace, statement, options, @@ -1381,17 +1467,11 @@ def handle_response(response_future, plan, timeout, errors, - hosts) - when Protocol::BatchRequest - batch_by_plan(promise, - keyspace, - statement, - options, - request, - plan, - timeout, - errors, - hosts) + hosts, + retries) + else + promise.break(ex) + end else promise.break(ex) end @@ -1409,7 +1489,7 @@ def wait_for_schema_agreement(connection, schedule) unless local.empty? host = @registry.host(connection.host) - if host && @load_balancing_policy.distance(host) != :ignore + if host && @profile_manager.distance(host) != :ignore versions << version = local.first['schema_version'] @logger.debug("Host #{host.ip} schema version is #{version}") end @@ -1417,7 +1497,7 @@ def wait_for_schema_agreement(connection, schedule) peers.each do |row| host = @registry.host(peer_ip(row)) - next unless host && @load_balancing_policy.distance(host) != :ignore + next unless host && @profile_manager.distance(host) != :ignore versions << version = row['schema_version'] @logger.debug("Host #{host.ip} schema version is #{version}") @@ -1497,7 +1577,7 @@ def prepare_statement(host, connection, cql, timeout) when Protocol::PreparedResultResponse id = r.id synchronize do - @prepared_statements[host][cql] = id + @prepared_statements[cql] = id @preparing_statements[host].delete(cql) end id diff --git a/lib/cassandra/cluster/connection_pool.rb b/lib/cassandra/cluster/connection_pool.rb index 791c6c60d..262fac082 100644 --- a/lib/cassandra/cluster/connection_pool.rb +++ b/lib/cassandra/cluster/connection_pool.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/connector.rb b/lib/cassandra/cluster/connector.rb index 059c93f6c..3b73c3318 100644 --- a/lib/cassandra/cluster/connector.rb +++ b/lib/cassandra/cluster/connector.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -122,11 +122,8 @@ def do_connect(host) ssl: @connection_options.ssl) do |connection| raise Errors::ClientError, 'Not connected, reactor stopped' unless connection - if @connection_options.nodelay? - connection.to_io.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) - else - connection.to_io.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 0) - end + connection.to_io.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, + @connection_options.nodelay? ? 1 : 0) Protocol::CqlProtocolHandler.new(connection, @reactor, @@ -134,7 +131,8 @@ def do_connect(host) @connection_options.compressor, @connection_options.heartbeat_interval, @connection_options.idle_timeout, - @connection_options.requests_per_connection) + @connection_options.requests_per_connection, + @connection_options.custom_type_handlers) end.flat_map do |connection| # connection is a CqlProtocolHandler f = request_options(connection) @@ -154,14 +152,14 @@ def do_connect(host) supported_cql_versions.first : '3.1.0' - startup_connection(connection, cql_version, compression) + startup_connection(host, connection, cql_version, compression) end f.fallback do |error| case error when Errors::ProtocolError synchronize do current_version = connection.protocol_version - if current_version > 1 + if current_version > 1 && @connection_options.protocol_negotiable? @logger.info("Host #{host.ip} doesn't support protocol version " \ "#{current_version}, downgrading") @@ -172,7 +170,7 @@ def do_connect(host) # don't want to accidentally raise it, so we update it to the min # of itself and current-1. @connection_options.protocol_version = - [@connection_options.protocol_version, current_version - 1].min + [@connection_options.protocol_version, current_version - 1].min do_connect(host) else Ione::Future.failed(error) @@ -200,7 +198,7 @@ def do_connect(host) end end - def startup_connection(connection, cql_version, compression) + def startup_connection(host, connection, cql_version, compression) connection.send_request(Protocol::StartupRequest.new(cql_version, compression), @execution_options.timeout).flat_map do |r| case r @@ -213,12 +211,9 @@ def startup_connection(connection, cql_version, compression) Ione::Future.failed(cannot_authenticate_error) end else - authenticator = @connection_options.create_authenticator( - r.authentication_class) + authenticator = @connection_options.create_authenticator(r.authentication_class, host) if authenticator - challenge_response_cycle(connection, - authenticator, - authenticator.initial_response) + challenge_response_cycle(connection, authenticator, authenticator.initial_response) else Ione::Future.failed(cannot_authenticate_error) end @@ -227,10 +222,12 @@ def startup_connection(connection, cql_version, compression) ::Ione::Future.resolved(connection) when Protocol::ErrorResponse ::Ione::Future.failed( - r.to_error(nil, VOID_STATEMENT, VOID_OPTIONS, EMPTY_LIST, :one, 0)) + r.to_error(nil, VOID_STATEMENT, VOID_OPTIONS, EMPTY_LIST, :one, 0) + ) else ::Ione::Future.failed( - Errors::InternalError.new("Unexpected response #{r.inspect}")) + Errors::InternalError.new("Unexpected response #{r.inspect}") + ) end end end @@ -246,7 +243,8 @@ def cannot_authenticate_error VOID_OPTIONS, EMPTY_LIST, :one, - 0) + 0 + ) end def request_options(connection) @@ -283,7 +281,7 @@ def challenge_response_cycle(connection, authenticator, token) case r when Protocol::AuthChallengeResponse token = authenticator.challenge_response(r.token) - challenge_response_cycle(pending_connection, authenticator, token) + challenge_response_cycle(connection, authenticator, token) when Protocol::AuthSuccessResponse begin authenticator.authentication_successful(r.token) @@ -293,10 +291,12 @@ def challenge_response_cycle(connection, authenticator, token) ::Ione::Future.resolved(connection) when Protocol::ErrorResponse ::Ione::Future.failed( - r.to_error(nil, VOID_STATEMENT, VOID_OPTIONS, EMPTY_LIST, :one, 0)) + r.to_error(nil, VOID_STATEMENT, VOID_OPTIONS, EMPTY_LIST, :one, 0) + ) else ::Ione::Future.failed( - Errors::InternalError.new("Unexpected response #{r.inspect}")) + Errors::InternalError.new("Unexpected response #{r.inspect}") + ) end end end diff --git a/lib/cassandra/cluster/control_connection.rb b/lib/cassandra/cluster/control_connection.rb index 34d82c3c6..9ff1fcfc2 100644 --- a/lib/cassandra/cluster/control_connection.rb +++ b/lib/cassandra/cluster/control_connection.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,8 +36,10 @@ def initialize(logger, io_reactor, cluster_registry, cluster_schema, @address_resolver = address_resolution_policy @connector = connector @connection_options = connection_options + @connection = nil @schema_fetcher = schema_fetcher @refreshing_statuses = ::Hash.new(false) + @refresh_schema_future = nil @status = :closed @refreshing_hosts = false @refreshing_host = ::Hash.new(false) @@ -88,7 +90,8 @@ def host_up(host) (@status == :closing || @status == :closed) || @load_balancing_policy.distance(host) == :ignore return connect_to_first_available( - @load_balancing_policy.plan(nil, VOID_STATEMENT, VOID_OPTIONS)) + @load_balancing_policy.plan(nil, VOID_STATEMENT, VOID_OPTIONS) + ) end end @@ -146,21 +149,23 @@ def inspect private - SELECT_LOCAL = Protocol::QueryRequest.new( - 'SELECT rack, data_center, host_id, release_version, tokens, partitioner ' \ + SELECT_LOCAL = Protocol::QueryRequest.new( + 'SELECT * ' \ 'FROM system.local', EMPTY_LIST, EMPTY_LIST, - :one) - SELECT_PEERS = Protocol::QueryRequest.new( - 'SELECT peer, rack, data_center, host_id, rpc_address, release_version, tokens ' \ + :one + ) + SELECT_PEERS = Protocol::QueryRequest.new( + 'SELECT * ' \ 'FROM system.peers', EMPTY_LIST, EMPTY_LIST, - :one) + :one + ) SELECT_PEER_QUERY = - 'SELECT rack, data_center, host_id, rpc_address, release_version, tokens ' \ + 'SELECT * ' \ 'FROM system.peers ' \ "WHERE peer = '%s'".freeze @@ -191,18 +196,14 @@ def reconnect_async(schedule) def register_async connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? request = Protocol::RegisterRequest.new( Protocol::TopologyChangeEventResponse::TYPE, Protocol::StatusChangeEventResponse::TYPE ) - if @connection_options.synchronize_schema? - request.events << Protocol::SchemaChangeEventResponse::TYPE - end + request.events << Protocol::SchemaChangeEventResponse::TYPE if @connection_options.synchronize_schema? f = connection.send_request(request) f = f.map do |r| @@ -224,25 +225,26 @@ def register_async else case event.change when 'UP' - address = event.address - + address = @address_resolver.resolve(event.address) if @registry.has_host?(address) @registry.host_up(address) else - refresh_host_async_maybe_retry(address) + refresh_host_async_maybe_retry(event.address) refresh_schema_async_wrapper end when 'DOWN' - @registry.host_down(event.address) + # RUBY-164: Don't mark host down if there are active connections. We have + # logic in connector.rb to call host_down when all connections to a node are lost, + # so that covers the requirement. when 'NEW_NODE' - address = event.address + address = @address_resolver.resolve(event.address) unless @registry.has_host?(address) - refresh_host_async_maybe_retry(address) + refresh_host_async_maybe_retry(event.address) refresh_schema_async_wrapper end when 'REMOVED_NODE' - @registry.host_lost(event.address) + @registry.host_lost(@address_resolver.resolve(event.address)) refresh_schema_async_wrapper end end @@ -257,9 +259,7 @@ def refresh_schema_async @logger.info('Refreshing schema') - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @schema_fetcher.fetch(connection).map do |keyspaces| @schema.replace(keyspaces) @@ -271,9 +271,7 @@ def refresh_schema_async def refresh_keyspace_async(keyspace_name) connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info("Refreshing keyspace \"#{keyspace_name}\"") @@ -284,16 +282,14 @@ def refresh_keyspace_async(keyspace_name) @schema.delete_keyspace(keyspace_name) end - @logger.info("Refreshed keyspace \"#{keyspace_name}\"") + @logger.info("Completed refreshing keyspace \"#{keyspace_name}\"") end end def refresh_table_async(keyspace_name, table_name) connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info("Refreshing table \"#{keyspace_name}.#{table_name}\"") @@ -304,16 +300,32 @@ def refresh_table_async(keyspace_name, table_name) @schema.delete_table(keyspace_name, table_name) end - @logger.info("Refreshed table \"#{keyspace_name}.#{table_name}\"") + @logger.info("Completed refreshing table \"#{keyspace_name}.#{table_name}\"") end end - def refresh_type_async(keyspace_name, type_name) + def refresh_materialized_view_async(keyspace_name, view_name) connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? + + @logger.info("Refreshing materialized view \"#{keyspace_name}.#{view_name}\"") + + @schema_fetcher.fetch_materialized_view(connection, keyspace_name, view_name).map do |view| + if view + @schema.replace_materialized_view(view) + else + @schema.delete_materialized_view(keyspace_name, view_name) + end + + @logger.info("Completed refreshing materialized view \"#{keyspace_name}.#{view_name}\"") end + end + + def refresh_type_async(keyspace_name, type_name) + connection = @connection + + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info("Refreshing user-defined type \"#{keyspace_name}.#{type_name}\"") @@ -324,16 +336,14 @@ def refresh_type_async(keyspace_name, type_name) @schema.delete_type(keyspace_name, type_name) end - @logger.info("Refreshed user-defined type \"#{keyspace_name}.#{type_name}\"") + @logger.info("Completed refreshing user-defined type \"#{keyspace_name}.#{type_name}\"") end end def refresh_function_async(keyspace_name, function_name, function_args) connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info('Refreshing user-defined function ' \ "\"#{keyspace_name}.#{function_name}\"") @@ -351,7 +361,7 @@ def refresh_function_async(keyspace_name, function_name, function_args) @schema.delete_function(keyspace_name, function_name, parsed_function_args) end - @logger.info('Refreshed user-defined function ' \ + @logger.info('Completed refreshing user-defined function ' \ "\"#{keyspace_name}.#{function_name}(#{function_args.join(',')})\"") end end @@ -359,9 +369,7 @@ def refresh_function_async(keyspace_name, function_name, function_args) def refresh_aggregate_async(keyspace_name, aggregate_name, aggregate_args) connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info('Refreshing user-defined aggregate ' \ "\"#{keyspace_name}.#{aggregate_name}\"") @@ -380,7 +388,7 @@ def refresh_aggregate_async(keyspace_name, aggregate_name, aggregate_args) @schema.delete_aggregate(keyspace_name, aggregate_name, parsed_aggregate_args) end - @logger.info('Refreshed user-defined aggregate ' \ + @logger.info('Completed refreshing user-defined aggregate ' \ "\"#{keyspace_name}.#{aggregate_name}(#{aggregate_args.join(',')})\"") end end @@ -432,9 +440,7 @@ def refresh_peers_async_retry(error, schedule) def refresh_peers_async connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info('Refreshing peers metadata') @@ -445,7 +451,7 @@ def refresh_peers_async peers.shuffle! peers.each do |data| - ip = peer_ip(data) + ip = peer_ip(data, connection.host) next unless ip ips << ip @registry.host_found(ip, data) @@ -456,7 +462,7 @@ def refresh_peers_async @registry.host_lost(host.ip) unless ips.include?(host.ip) end - @logger.info('Refreshed peers metadata') + @logger.info('Completed refreshing peers metadata') nil end @@ -465,22 +471,18 @@ def refresh_peers_async def refresh_metadata_async connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? @logger.info("Refreshing connected host's metadata") send_select_request(connection, SELECT_LOCAL).map do |local| - if local.empty? - raise Errors::InternalError, "Unable to fetch connected host's metadata" - else - data = local.first - @registry.host_found(IPAddr.new(connection.host), data) - @metadata.update(data) - end + raise Errors::InternalError, "Unable to fetch connected host's metadata" if local.empty? + + data = local.first + @registry.host_found(@address_resolver.resolve(IPAddr.new(connection.host)), data) + @metadata.update(data) - @logger.info("Refreshed connected host's metadata") + @logger.info("Completed refreshing connected host's metadata") nil end @@ -513,6 +515,9 @@ def refresh_host_status_with_retry(original_timer, host, schedule) end end + # @param address [IPAddr] address node address, as reported from a C* event. Thus it is *not* resolved + # relative to the client, but rather is the address that other nodes would use to communicate with + # this node. def refresh_host_async_maybe_retry(address) synchronize do return Ione::Future.resolved if @refreshing_hosts || @refreshing_host[address] @@ -536,6 +541,9 @@ def refresh_host_async_maybe_retry(address) end end + # @param address [IPAddr] address node address, as reported from a C* event. Thus it is *not* resolved + # relative to the client, but rather is the address that other nodes would use to communicate with + # this node. def refresh_host_async_retry(address, error, schedule) timeout = schedule.next @logger.info("Failed to refresh host #{address} (#{error.class.name}: " \ @@ -557,11 +565,12 @@ def refresh_host_async_retry(address, error, schedule) end end + # @param address [IPAddr] address node address, as reported from a C* event. Thus it is *not* resolved + # relative to the client, but rather is the address that other nodes would use to communicate with + # this node. def refresh_host_async(address) connection = @connection - if connection.nil? - return Ione::Future.failed(Errors::ClientError.new('Not connected')) - end + return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil? ip = address.to_s @@ -577,12 +586,15 @@ def refresh_host_async(address) end send_select_request(connection, request).map do |rows| - if rows.empty? - raise Errors::InternalError, "Unable to find host metadata: #{ip}" - else - @logger.info("Refreshed host metadata: #{ip}") - @registry.host_found(address, rows.first) - end + raise Errors::InternalError, "Unable to find host metadata: #{ip}" if rows.empty? + + @logger.info("Completed refreshing host metadata: #{ip}") + address = if ip == connection.host + @address_resolver.resolve(address) + else + peer_ip(rows.first, connection.host) + end + @registry.host_found(address, rows.first) self end @@ -647,9 +659,7 @@ def connect_to_first_available(plan, errors = nil) end f = f.flat_map { register_async } f = f.flat_map { refresh_peers_async_maybe_retry } - if @connection_options.synchronize_schema? - f = f.flat_map { refresh_maybe_retry(:schema) } - end + f = f.flat_map { refresh_maybe_retry(:schema) } if @connection_options.synchronize_schema? f = f.fallback do |error| @logger.debug("Connection to #{host.ip} failed " \ "(#{error.class.name}: #{error.message})") @@ -675,16 +685,37 @@ def connect_to_host(host) @connector.connect(host) end - def peer_ip(data) - ip = data['rpc_address'] - ip = data['peer'] if ip == '0.0.0.0' + def peer_ip(data, host_address) + peer = data['peer'] + + return nil unless peer && data['host_id'] && data['data_center'] && data['rack'] && data['tokens'] + + rpc_address = data['rpc_address'] + + if rpc_address.nil? + @logger.info("The system.peers row for '#{data['peer']}' has no rpc_address. This is likely " \ + 'a gossip or snitch issue. This host will be ignored.') + return nil + end + + if peer == host_address || rpc_address == host_address + # Some DSE versions were inserting a line for the local node in peers (with mostly null values). + # This has been fixed, but if we detect that's the case, ignore it as it's not really a big deal. + + @logger.debug("System.peers on node #{host_address} has a line for itself. This is not normal but is a " \ + 'known problem of some DSE versions. Ignoring the entry.') + return nil + end + + ip = rpc_address + ip = peer if ip == '0.0.0.0' @address_resolver.resolve(ip) end def process_schema_changes(schema_changes) - refresh_keyspaces = ::Hash.new - refresh_tables = ::Hash.new + refresh_keyspaces = ::Hash.new + refresh_tables_and_views = ::Hash.new refresh_types = ::Hash.new # This hash is of the form @@ -700,14 +731,15 @@ def process_schema_changes(schema_changes) case change.target when Protocol::Constants::SCHEMA_CHANGE_TARGET_KEYSPACE - refresh_tables.delete(keyspace) + refresh_tables_and_views.delete(keyspace) refresh_types.delete(keyspace) refresh_functions.delete(keyspace) refresh_aggregates.delete(keyspace) refresh_keyspaces[keyspace] = true when Protocol::Constants::SCHEMA_CHANGE_TARGET_TABLE - tables = refresh_tables[keyspace] ||= ::Hash.new - tables[change.name] = true + # We can't distinguish between table and view change events, so refresh both. + tables_and_views = refresh_tables_and_views[keyspace] ||= ::Hash.new + tables_and_views[change.name] = true when Protocol::Constants::SCHEMA_CHANGE_TARGET_UDT types = refresh_types[keyspace] ||= ::Hash.new types[change.name] = true @@ -726,9 +758,10 @@ def process_schema_changes(schema_changes) futures << refresh_maybe_retry(:keyspace, keyspace) end - refresh_tables.each do |(keyspace, tables)| - tables.each_key do |table| - futures << refresh_maybe_retry(:table, keyspace, table) + refresh_tables_and_views.each do |(keyspace, tables_and_views)| + tables_and_views.each_key do |table_or_view| + futures << refresh_maybe_retry(:table, keyspace, table_or_view) + futures << refresh_maybe_retry(:materialized_view, keyspace, table_or_view) end end diff --git a/lib/cassandra/cluster/failed_connection.rb b/lib/cassandra/cluster/failed_connection.rb index 36f057e54..e7e6f7da6 100644 --- a/lib/cassandra/cluster/failed_connection.rb +++ b/lib/cassandra/cluster/failed_connection.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/metadata.rb b/lib/cassandra/cluster/metadata.rb index 230ed5384..c93395e34 100644 --- a/lib/cassandra/cluster/metadata.rb +++ b/lib/cassandra/cluster/metadata.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,9 +39,7 @@ def initialize(cluster_registry, end def find_replicas(keyspace, statement) - unless statement.respond_to?(:partition_key) && statement.respond_to?(:keyspace) - return EMPTY_LIST - end + return EMPTY_LIST unless statement.respond_to?(:partition_key) && statement.respond_to?(:keyspace) keyspace = String(statement.keyspace || keyspace) partition_key = statement.partition_key @@ -63,7 +61,7 @@ def find_replicas(keyspace, statement) end def update(data) - @name = data['name'] + @name = data['cluster_name'] @partitioner = @partitioners[data['partitioner']] self diff --git a/lib/cassandra/cluster/options.rb b/lib/cassandra/cluster/options.rb index e7493a173..4d12dd4e0 100644 --- a/lib/cassandra/cluster/options.rb +++ b/lib/cassandra/cluster/options.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,9 +20,12 @@ module Cassandra class Cluster # @private class Options + extend AttrBoolean + attr_reader :auth_provider, :compressor, :connect_timeout, :credentials, :heartbeat_interval, :idle_timeout, :port, :schema_refresh_delay, - :schema_refresh_timeout, :ssl + :schema_refresh_timeout, :ssl, :custom_type_handlers + attr_boolean :protocol_negotiable, :synchronize_schema, :nodelay, :allow_beta_protocol attr_accessor :protocol_version @@ -41,9 +44,10 @@ def initialize(logger, synchronize_schema, schema_refresh_delay, schema_refresh_timeout, - client_timestamps, nodelay, - requests_per_connection) + requests_per_connection, + custom_types, + allow_beta_protocol) @logger = logger @protocol_version = protocol_version @credentials = credentials @@ -57,32 +61,43 @@ def initialize(logger, @synchronize_schema = synchronize_schema @schema_refresh_delay = schema_refresh_delay @schema_refresh_timeout = schema_refresh_timeout - @client_timestamps = client_timestamps @nodelay = nodelay + @allow_beta_protocol = allow_beta_protocol + @custom_type_handlers = {} + custom_types.each do |type_klass| + @custom_type_handlers[type_klass.type] = type_klass + end @connections_per_local_node = connections_per_local_node @connections_per_remote_node = connections_per_remote_node @requests_per_connection = requests_per_connection - end - - def synchronize_schema? - @synchronize_schema - end - def client_timestamps? - @client_timestamps - end + # If @protocol_version is nil, it means we want the driver to negotiate the + # protocol starting with our known max. If @protocol_version is not nil, + # it means the user wants us to use a particular version, so we should not + # support negotiation. - def nodelay? - @nodelay + @protocol_negotiable = @protocol_version.nil? + @protocol_version ||= allow_beta_protocol ? + Cassandra::Protocol::Versions::BETA_VERSION : + Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION end def compression @compressor && @compressor.algorithm end - def create_authenticator(authentication_class) - @auth_provider && @auth_provider.create_authenticator(authentication_class) + def create_authenticator(authentication_class, host) + if @auth_provider + # Auth providers should take an auth-class and host, but they used to not, so for backward compatibility + # we figure out if this provider does, and if so send both args, otherwise just send the auth-class. + + if @auth_provider.method(:create_authenticator).arity == 1 + @auth_provider.create_authenticator(authentication_class) + else + @auth_provider.create_authenticator(authentication_class, host) + end + end end def connections_per_local_node diff --git a/lib/cassandra/cluster/registry.rb b/lib/cassandra/cluster/registry.rb index c3e5217fb..debe0bfec 100644 --- a/lib/cassandra/cluster/registry.rb +++ b/lib/cassandra/cluster/registry.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -60,14 +60,18 @@ def each_host(&block) end alias hosts each_host + # @param address [IPAddr] resolved address of the node def host(address) @hosts[address.to_s] end + # @param address [IPAddr] resolved address of the node def has_host?(address) @hosts.key?(address.to_s) end + # @param address [IPAddr] resolved address of the node + # @param data [Hash] attributes of the host, typically a row from system.local or system.peers. def host_found(address, data = {}) ip = address.to_s host = @hosts[ip] @@ -76,7 +80,9 @@ def host_found(address, data = {}) if host.id == data['host_id'] && host.release_version == data['release_version'] && host.rack == data['rack'] && - host.datacenter == data['data_center'] + host.datacenter == data['data_center'] && + host.broadcast_address == data['broadcast_address'] && + host.listen_address == data['listen_address'] return self if host.up? @@ -106,6 +112,7 @@ def host_found(address, data = {}) self end + # @param address [IPAddr] resolved address of the node def host_down(address) ip = address.to_s host = @hosts[ip] @@ -123,6 +130,7 @@ def host_down(address) self end + # @param address [IPAddr] resolved address of the node def host_up(address) ip = address.to_s host = @hosts[ip] @@ -140,6 +148,7 @@ def host_up(address) self end + # @param address [IPAddr] resolved address of the node def host_lost(address) ip = address.to_s host = nil @@ -159,6 +168,8 @@ def host_lost(address) private + # @param ip [String] resolved address of the node + # @param data [Hash] attributes of the host, typically a row from system.local or system.peers. def create_host(ip, data) Host.new(ip, data['host_id'], @@ -166,9 +177,12 @@ def create_host(ip, data) data['data_center'], data['release_version'], Array(data['tokens']).freeze, - :up) + :up, + data['broadcast_address'], + data['listen_address']) end + # @param ip [Host] host that is found to be up. def toggle_up(host) host = Host.new(host.ip, host.id, @@ -176,7 +190,9 @@ def toggle_up(host) host.datacenter, host.release_version, host.tokens, - :up) + :up, + host.broadcast_address, + host.listen_address) @logger.debug("Host #{host.ip} is up") @listeners.each do |listener| begin @@ -188,6 +204,7 @@ def toggle_up(host) host end + # @param ip [Host] host that is found to be down. def toggle_down(host) host = Host.new(host.ip, host.id, @@ -195,7 +212,9 @@ def toggle_down(host) host.datacenter, host.release_version, host.tokens, - :down) + :down, + host.broadcast_address, + host.listen_address) @logger.debug("Host #{host.ip} is down") @listeners.reverse_each do |listener| begin @@ -207,6 +226,7 @@ def toggle_down(host) host end + # @param ip [Host] host that is lost. def notify_lost(host) if host.up? @logger.debug("Host #{host.ip} is down and lost") @@ -216,7 +236,9 @@ def notify_lost(host) host.datacenter, host.release_version, host.tokens, - :down) + :down, + host.broadcast_address, + host.listen_address) @listeners.reverse_each do |listener| begin listener.host_down(host) @@ -241,6 +263,7 @@ def notify_lost(host) end end + # @param ip [Host] host that is found. def notify_found(host) @logger.debug("Host #{host.ip} is found and up") @listeners.each do |listener| diff --git a/lib/cassandra/cluster/schema.rb b/lib/cassandra/cluster/schema.rb index 961fe90e5..845ebbdbd 100644 --- a/lib/cassandra/cluster/schema.rb +++ b/lib/cassandra/cluster/schema.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -126,7 +126,7 @@ def delete_keyspace(keyspace_name) end def replace_table(table) - keyspace = @keyspaces[table.keyspace] + keyspace = table.keyspace return self unless keyspace @@ -169,6 +169,50 @@ def delete_table(keyspace_name, table_name) self end + def replace_materialized_view(view) + keyspace = view.keyspace + + return self unless keyspace + + old_view = keyspace.materialized_view(view.name) + + return self if old_view == view + + keyspace = keyspace.update_materialized_view(view) + + synchronize do + keyspaces = @keyspaces.dup + keyspaces[keyspace.name] = keyspace + @keyspaces = keyspaces + end + + keyspace_changed(keyspace) + + self + end + + def delete_materialized_view(keyspace_name, view_name) + keyspace = @keyspaces[keyspace_name] + + return self unless keyspace + + view = keyspace.materialized_view(view_name) + + return self unless view + + keyspace = keyspace.delete_materialized_view(view_name) + + synchronize do + keyspaces = @keyspaces.dup + keyspaces[keyspace_name] = keyspace + @keyspaces = keyspaces + end + + keyspace_changed(keyspace) + + self + end + def replace_type(type) keyspace = @keyspaces[type.keyspace] diff --git a/lib/cassandra/cluster/schema/cql_type_parser.rb b/lib/cassandra/cluster/schema/cql_type_parser.rb index ed399f499..8fc17d70c 100644 --- a/lib/cassandra/cluster/schema/cql_type_parser.rb +++ b/lib/cassandra/cluster/schema/cql_type_parser.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -72,6 +72,9 @@ def lookup_type(node, types) Cassandra::Types.tuple(*node.children.map { |t| lookup_type(t, types)}) when 'empty' then Cassandra::Types.custom('org.apache.cassandra.db.marshal.EmptyType') + when /\A'/ then + # Custom type. + Cassandra::Types.custom(node.name[1..-2]) else types.fetch(node.name) do raise IncompleteTypeError, "unable to lookup type #{node.name.inspect}" @@ -97,7 +100,7 @@ def parse_node(string) when ' ' next else - node.name << char + node.name << char unless char == '"' end end diff --git a/lib/cassandra/cluster/schema/fetchers.rb b/lib/cassandra/cluster/schema/fetchers.rb index e4d115cda..c489a879a 100644 --- a/lib/cassandra/cluster/schema/fetchers.rb +++ b/lib/cassandra/cluster/schema/fetchers.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,20 +26,25 @@ module Fetcher COMPRESSION_PACKAGE_PREFIX = 'org.apache.cassandra.io.compress.'.freeze def fetch(connection) + # rubocop:disable Metrics/LineLength Ione::Future.all(select_keyspaces(connection), select_tables(connection), select_columns(connection), select_types(connection), select_functions(connection), - select_aggregates(connection)) - .map do |(rows_keyspaces, rows_tables, rows_columns, - rows_types, rows_functions, rows_aggregates)| - + select_aggregates(connection), + select_materialized_views(connection), + select_indexes(connection), + select_triggers(connection)) + .map do |rows_keyspaces, rows_tables, rows_columns, rows_types, rows_functions, rows_aggregates, rows_views, rows_indexes, rows_triggers| lookup_tables = map_rows_by(rows_tables, 'keyspace_name') lookup_columns = map_rows_by(rows_columns, 'keyspace_name') lookup_types = map_rows_by(rows_types, 'keyspace_name') lookup_functions = map_rows_by(rows_functions, 'keyspace_name') lookup_aggregates = map_rows_by(rows_aggregates, 'keyspace_name') + lookup_views = map_rows_by(rows_views, 'keyspace_name') + lookup_indexes = map_rows_by(rows_indexes, 'keyspace_name') + lookup_triggers = map_rows_by(rows_triggers, 'keyspace_name') rows_keyspaces.map do |keyspace_data| name = keyspace_data['keyspace_name'] @@ -49,7 +54,10 @@ def fetch(connection) lookup_columns[name], lookup_types[name], lookup_functions[name], - lookup_aggregates[name]) + lookup_aggregates[name], + lookup_views[name], + lookup_indexes[name], + lookup_triggers[name]) end end end @@ -60,9 +68,11 @@ def fetch_keyspace(connection, keyspace_name) select_keyspace_columns(connection, keyspace_name), select_keyspace_types(connection, keyspace_name), select_keyspace_functions(connection, keyspace_name), - select_keyspace_aggregates(connection, keyspace_name)) - .map do |(rows_keyspaces, rows_tables, rows_columns, - rows_types, rows_functions, rows_aggregates)| + select_keyspace_aggregates(connection, keyspace_name), + select_keyspace_materialized_views(connection, keyspace_name), + select_keyspace_indexes(connection, keyspace_name), + select_keyspace_triggers(connection, keyspace_name)) + .map do |rows_keyspaces, rows_tables, rows_columns, rows_types, rows_functions, rows_aggregates, rows_views, rows_indexes, rows_triggers| if rows_keyspaces.empty? nil else @@ -71,20 +81,41 @@ def fetch_keyspace(connection, keyspace_name) rows_columns, rows_types, rows_functions, - rows_aggregates) + rows_aggregates, + rows_views, + rows_indexes, + rows_triggers) end end end def fetch_table(connection, keyspace_name, table_name) Ione::Future.all(select_table(connection, keyspace_name, table_name), - select_table_columns(connection, keyspace_name, table_name)) - .map do |(rows_tables, rows_columns)| + select_table_columns(connection, keyspace_name, table_name), + select_table_indexes(connection, keyspace_name, table_name), + select_table_triggers(connection, keyspace_name, table_name)) + .map do |(rows_tables, rows_columns, rows_indexes, rows_triggers)| if rows_tables.empty? nil else create_table(rows_tables.first, - rows_columns) + rows_columns, + rows_indexes, + rows_triggers) + end + end + end + + def fetch_materialized_view(connection, keyspace_name, view_name) + Ione::Future.all(select_materialized_view(connection, keyspace_name, view_name), + select_table_columns(connection, keyspace_name, view_name)) + .map do |rows_views, rows_columns| + if rows_views.empty? + nil + else + view_row = rows_views.first + create_materialized_view(view_row, + rows_columns) end end end @@ -100,8 +131,8 @@ def fetch_type(connection, keyspace_name, type_name) end def fetch_function(connection, keyspace_name, function_name, function_args) - select_function(connection, keyspace_name, function_name, function_args). - map do |rows_functions| + select_function(connection, keyspace_name, function_name, function_args) + .map do |rows_functions| if rows_functions.empty? nil else @@ -111,13 +142,13 @@ def fetch_function(connection, keyspace_name, function_name, function_args) end def fetch_aggregate(connection, keyspace_name, aggregate_name, aggregate_args) - select_aggregate(connection, keyspace_name, aggregate_name, aggregate_args). - map do |rows_aggregates| + select_aggregate(connection, keyspace_name, aggregate_name, aggregate_args) + .map do |rows_aggregates| if rows_aggregates.empty? nil else - create_aggregate(rows_aggregates.first, @schema.keyspace(keyspace_name). - send(:raw_functions)) + create_aggregate(rows_aggregates.first, @schema.keyspace(keyspace_name) + .send(:raw_functions)) end end end @@ -132,10 +163,22 @@ def select_tables(connection) FUTURE_EMPTY_LIST end + def select_materialized_views(connection) + FUTURE_EMPTY_LIST + end + def select_columns(connection) FUTURE_EMPTY_LIST end + def select_indexes(connection) + FUTURE_EMPTY_LIST + end + + def select_triggers(connection) + FUTURE_EMPTY_LIST + end + def select_types(connection) FUTURE_EMPTY_LIST end @@ -156,10 +199,22 @@ def select_keyspace_tables(connection, keyspace_name) FUTURE_EMPTY_LIST end + def select_keyspace_materialized_views(connection, keyspace_name) + FUTURE_EMPTY_LIST + end + def select_keyspace_columns(connection, keyspace_name) FUTURE_EMPTY_LIST end + def select_keyspace_indexes(connection, keyspace_name) + FUTURE_EMPTY_LIST + end + + def select_keyspace_triggers(connection, keyspace_name) + FUTURE_EMPTY_LIST + end + def select_keyspace_types(connection, keyspace_name) FUTURE_EMPTY_LIST end @@ -176,10 +231,22 @@ def select_table(connection, keyspace_name, table_name) FUTURE_EMPTY_LIST end + def select_materialized_view(connection, keyspace_name, view_name) + FUTURE_EMPTY_LIST + end + def select_table_columns(connection, keyspace_name, table_name) FUTURE_EMPTY_LIST end + def select_table_indexes(connection, keyspace_name, table_name) + FUTURE_EMPTY_LIST + end + + def select_table_triggers(connection, keyspace_name, table_name) + FUTURE_EMPTY_LIST + end + def select_type(connection, keyspace_name, type_name) FUTURE_EMPTY_LIST end @@ -195,7 +262,8 @@ def select_aggregate(connection, keyspace_name, aggregate_name, aggregate_args) def send_select_request(connection, cql, params = EMPTY_LIST, types = EMPTY_LIST) backtrace = caller connection.send_request( - Protocol::QueryRequest.new(cql, params, types, :one)).map do |r| + Protocol::QueryRequest.new(cql, params, types, :one) + ).map do |r| case r when Protocol::RowsResultResponse r.rows @@ -212,43 +280,44 @@ def send_select_request(connection, cql, params = EMPTY_LIST, types = EMPTY_LIST def map_rows_by(rows, key_name, &block) rows.each_with_object(::Hash.new { EMPTY_LIST }) do |row, map| key = row[key_name] - map[key] = [] unless map.has_key?(key) + map[key] = [] unless map.key?(key) - if block - map[key] << yield(row) - else - map[key] << row - end + map[key] << if block + yield(row) + else + row + end end end end # @private module Fetchers + # rubocop:disable Style/ClassAndModuleCamelCase class V1_2_x SELECT_KEYSPACES = 'SELECT * FROM system.schema_keyspaces'.freeze SELECT_TABLES = 'SELECT * FROM system.schema_columnfamilies'.freeze SELECT_COLUMNS = 'SELECT * FROM system.schema_columns'.freeze SELECT_KEYSPACE = - 'SELECT * ' \ - 'FROM system.schema_keyspaces ' \ - 'WHERE keyspace_name = \'%s\''.freeze + 'SELECT * ' \ + 'FROM system.schema_keyspaces ' \ + 'WHERE keyspace_name = \'%s\''.freeze SELECT_KEYSPACE_TABLES = - 'SELECT * ' \ - 'FROM system.schema_columnfamilies ' \ - 'WHERE keyspace_name = \'%s\''.freeze + 'SELECT * ' \ + 'FROM system.schema_columnfamilies ' \ + 'WHERE keyspace_name = \'%s\''.freeze SELECT_KEYSPACE_COLUMNS = - 'SELECT * FROM system.schema_columns WHERE keyspace_name = \'%s\''.freeze + 'SELECT * FROM system.schema_columns WHERE keyspace_name = \'%s\''.freeze SELECT_TABLE = - 'SELECT * ' \ - 'FROM system.schema_columnfamilies ' \ - 'WHERE keyspace_name = \'%s\' AND columnfamily_name = \'%s\''.freeze + 'SELECT * ' \ + 'FROM system.schema_columnfamilies ' \ + 'WHERE keyspace_name = \'%s\' AND columnfamily_name = \'%s\''.freeze SELECT_TABLE_COLUMNS = - 'SELECT * ' \ - 'FROM system.schema_columns ' \ - 'WHERE keyspace_name = \'%s\' AND columnfamily_name = \'%s\''.freeze + 'SELECT * ' \ + 'FROM system.schema_columns ' \ + 'WHERE keyspace_name = \'%s\' AND columnfamily_name = \'%s\''.freeze - include Fetcher + include Cassandra::Cluster::Schema::Fetcher def initialize(type_parser, schema) @type_parser = type_parser @@ -274,20 +343,20 @@ def select_keyspace(connection, keyspace_name) end def select_keyspace_tables(connection, keyspace_name) - send_select_request(connection, SELECT_KEYSPACE_TABLES % keyspace_name) + send_select_request(connection, format(SELECT_KEYSPACE_TABLES, keyspace_name)) end def select_keyspace_columns(connection, keyspace_name) - send_select_request(connection, SELECT_KEYSPACE_COLUMNS % keyspace_name) + send_select_request(connection, format(SELECT_KEYSPACE_COLUMNS, keyspace_name)) end def select_table(connection, keyspace_name, table_name) - send_select_request(connection, SELECT_TABLE % [keyspace_name, table_name]) + send_select_request(connection, format(SELECT_TABLE, keyspace_name, table_name)) end def select_table_columns(connection, keyspace_name, table_name) send_select_request(connection, - SELECT_TABLE_COLUMNS % [keyspace_name, table_name]) + format(SELECT_TABLE_COLUMNS, keyspace_name, table_name)) end def create_replication(keyspace_data) @@ -298,12 +367,13 @@ def create_replication(keyspace_data) end def create_keyspace(keyspace_data, rows_tables, rows_columns, - rows_types, rows_functions, rows_aggregates) + rows_types, rows_functions, rows_aggregates, + rows_views, rows_indexes, rows_triggers) keyspace_name = keyspace_data['keyspace_name'] replication = create_replication(keyspace_data) - types = rows_types.each_with_object({}) do |row, types| - types[row['type_name']] = create_type(row) - end + types = rows_types.each_with_object({}) do |row, h| + h[row['type_name']] = create_type(row) + end # Create a FunctionCollection for the functions and aggregates. functions = Cassandra::FunctionCollection.new @@ -317,9 +387,14 @@ def create_keyspace(keyspace_data, rows_tables, rows_columns, end lookup_columns = map_rows_by(rows_columns, 'columnfamily_name') - tables = rows_tables.each_with_object({}) do |row, tables| + lookup_indexes = map_rows_by(rows_indexes, 'columnfamily_name') + lookup_triggers = map_rows_by(rows_triggers, 'columnfamily_name') + tables = rows_tables.each_with_object({}) do |row, h| table_name = row['columnfamily_name'] - tables[table_name] = create_table(row, lookup_columns[table_name]) + h[table_name] = create_table(row, + lookup_columns[table_name], + lookup_indexes[table_name], + lookup_triggers[table_name]) end Keyspace.new(keyspace_name, @@ -328,24 +403,34 @@ def create_keyspace(keyspace_data, rows_tables, rows_columns, tables, types, functions, - aggregates) + aggregates, + {}) end - def create_table(table_data, rows_columns) + def create_table(table_data, rows_columns, rows_indexes, rows_triggers) keyspace_name = table_data['keyspace_name'] table_name = table_data['columnfamily_name'] key_validator = @type_parser.parse(table_data['key_validator']) comparator = @type_parser.parse(table_data['comparator']) column_aliases = ::JSON.load(table_data['column_aliases']) - if !comparator.collections.nil? + if comparator.collections.nil? + is_compact = true + if !column_aliases.empty? || rows_columns.empty? + has_value = true + clustering_size = comparator.results.size + else + has_value = false + clustering_size = 0 + end + else size = comparator.results.size if !comparator.collections.empty? is_compact = false has_value = false clustering_size = size - 2 elsif column_aliases.size == size - 1 && - comparator.results.last.first == Cassandra::Types.varchar + comparator.results.last.first == Cassandra::Types.varchar is_compact = false has_value = false clustering_size = size - 1 @@ -354,33 +439,24 @@ def create_table(table_data, rows_columns) has_value = (!column_aliases.empty? || rows_columns.empty?) clustering_size = size end - else - is_compact = true - if (!column_aliases.empty? || rows_columns.empty?) - has_value = true - clustering_size = comparator.results.size - else - has_value = false - clustering_size = 0 - end end + # Separate out the partition-key, clustering-columns, and other-columns partition_key = [] clustering_columns = [] clustering_order = [] + other_columns = [] compaction_strategy = create_compaction_strategy(table_data) table_options = - create_table_options(table_data, compaction_strategy, is_compact) - table_columns = {} - other_columns = [] + create_table_options(table_data, compaction_strategy, is_compact) key_aliases = ::JSON.load(table_data['key_aliases']) key_validator.results.each_with_index do |(type, order, is_frozen), i| - key_alias = key_aliases.fetch(i) { i.zero? ? "key" : "key#{i + 1}" } + key_alias = key_aliases.fetch(i) { i.zero? ? 'key' : "key#{i + 1}" } - partition_key[i] = Column.new(key_alias, type, order, nil, false, is_frozen) + partition_key[i] = Column.new(key_alias, type, order, false, is_frozen) end clustering_size.times do |i| @@ -388,7 +464,7 @@ def create_table(table_data, rows_columns) type, order, is_frozen = comparator.results.fetch(i) clustering_columns[i] = - Column.new(column_alias, type, order, nil, false, is_frozen) + Column.new(column_alias, type, order, false, is_frozen) clustering_order[i] = order end @@ -398,71 +474,87 @@ def create_table(table_data, rows_columns) unless value_alias.empty? type, order, is_frozen = - @type_parser.parse(table_data['default_validator']).results.first + @type_parser.parse(table_data['default_validator']).results.first other_columns << - Column.new(value_alias, type, order, nil, false, is_frozen) + Column.new(value_alias, type, order, false, is_frozen) end end + index_rows = [] rows_columns.each do |row| - other_columns << create_column(row) - end + column = create_column(row) + other_columns << column - partition_key.each do |column| - table_columns[column.name] = column + # In C* 1.2.x, index info is in the column metadata; rows_indexes is []. + index_rows << [column, row] unless row['index_type'].nil? end - clustering_columns.each do |column| - table_columns[column.name] = column + table = Cassandra::Table.new(@schema.keyspace(keyspace_name), + table_name, + partition_key, + clustering_columns, + other_columns, + table_options, + clustering_order, + table_data['id']) + + # Create Index objects and add them to the table. + index_rows.each do |column, row| + create_index(table, column, row) end + table + end - other_columns.each do |column| - table_columns[column.name] = column + def create_index(table, column, row_column) + # Most of this logic was taken from the Java driver. + options = {} + # For some versions of C*, this field could have a literal string 'null' value. + if !row_column['index_options'].nil? && row_column['index_options'] != 'null' && + !row_column['index_options'].empty? + options = ::JSON.load(row_column['index_options']) end - - Table.new(keyspace_name, - table_name, - partition_key, - clustering_columns, - table_columns, - table_options, - clustering_order) + column_name = Util.escape_name(column.name) + target = if options.key?('index_keys') + "keys(#{column_name})" + elsif options.key?('index_keys_and_values') + "entries(#{column_name})" + elsif column.frozen? && (column.type == Cassandra::Types::Set || + column.type == Cassandra::Types::List || + column.type == Cassandra::Types::Map) + "full(#{column_name})" + else + column_name + end + + table.add_index(Cassandra::Index.new(table, + row_column['index_name'], + row_column['index_type'].downcase.to_sym, + target, + options)) end def create_column(column_data) name = column_data['column_name'] is_static = (column_data['type'] == 'STATIC') type, order, is_frozen = - @type_parser.parse(column_data['validator']).results.first - - if column_data['index_type'].nil? - index = nil - elsif column_data['index_type'].to_s.upcase == 'CUSTOM' || - !column_data['index_options'] - index = Column::Index.new(column_data['index_name']) - else - options = ::JSON.load(column_data['index_options']) - index = Column::Index.new(column_data['index_name'], - options && options['class_name']) - end - - Column.new(name, type, order, index, is_static, is_frozen) + @type_parser.parse(column_data['validator']).results.first + Column.new(name, type, order, is_static, is_frozen) end def create_compaction_strategy(table_data) klass = table_data['compaction_strategy_class'] klass.slice!('org.apache.cassandra.db.compaction.') options = ::JSON.load(table_data['compaction_strategy_options']) - Table::Compaction.new(klass, options) + ColumnContainer::Compaction.new(klass, options) end def create_table_options(table_data, compaction_strategy, is_compact) compression_parameters = ::JSON.load(table_data['compression_parameters']) if compression_parameters['sstable_compression'] - compression_parameters['sstable_compression']. - slice!(COMPRESSION_PACKAGE_PREFIX) + compression_parameters['sstable_compression'] + .slice!(COMPRESSION_PACKAGE_PREFIX) end - Table::Options.new( + Cassandra::ColumnContainer::Options.new( table_data['comment'], table_data['read_repair_chance'], table_data['local_read_repair_chance'], @@ -479,88 +571,109 @@ def create_table_options(table_data, compaction_strategy, is_compact) nil, compaction_strategy, compression_parameters, - is_compact + is_compact, + table_data['crc_check_chance'], + table_data['extensions'], + nil ) end end class V2_0_x < V1_2_x + SELECT_TRIGGERS = 'SELECT * FROM system.schema_triggers'.freeze + SELECT_KEYSPACE = - 'SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_TABLES = - 'SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_COLUMNS = - 'SELECT * FROM system.schema_columns WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system.schema_columns WHERE keyspace_name = ?'.freeze + SELECT_KEYSPACE_TRIGGERS = + 'SELECT * FROM system.schema_triggers WHERE keyspace_name = ?'.freeze + SELECT_TABLE = - 'SELECT * ' \ - 'FROM system.schema_columnfamilies ' \ - 'WHERE keyspace_name = ? AND columnfamily_name = ?'.freeze + 'SELECT * ' \ + 'FROM system.schema_columnfamilies ' \ + 'WHERE keyspace_name = ? AND columnfamily_name = ?'.freeze SELECT_TABLE_COLUMNS = - 'SELECT * ' \ - 'FROM system.schema_columns ' \ - 'WHERE keyspace_name = ? AND columnfamily_name = ?'.freeze + 'SELECT * ' \ + 'FROM system.schema_columns ' \ + 'WHERE keyspace_name = ? AND columnfamily_name = ?'.freeze + SELECT_TABLE_TRIGGERS = + 'SELECT * ' \ + 'FROM system.schema_triggers ' \ + 'WHERE keyspace_name = ? AND columnfamily_name = ?'.freeze private - def create_table(table_data, rows_columns) + def create_table(table_data, rows_columns, rows_indexes, rows_triggers) keyspace_name = table_data['keyspace_name'] table_name = table_data['columnfamily_name'] comparator = @type_parser.parse(table_data['comparator']) - table_columns = {} - other_columns = [] clustering_size = 0 + # Separate out partition-key, clustering columns, other columns. partition_key = [] clustering_columns = [] clustering_order = [] + other_columns = [] + index_rows = [] rows_columns.each do |row| next if row['column_name'].empty? column = create_column(row) type = row['type'].to_s - index = row['component_index'] || 0 + ind = row['component_index'] || 0 case type.upcase when 'PARTITION_KEY' - partition_key[index] = column + partition_key[ind] = column when 'CLUSTERING_KEY' - clustering_columns[index] = column - clustering_order[index] = column.order + clustering_columns[ind] = column + clustering_order[ind] = column.order - if clustering_size.zero? || index == clustering_size - clustering_size = index + 1 - end + clustering_size += 1 else other_columns << column end - end - partition_key.each do |column| - table_columns[column.name] = column + # In C* 2.0.x, index info is in the column metadata; rows_indexes is nil. + index_rows << [column, row] unless row['index_type'].nil? end - clustering_columns.each do |column| - table_columns[column.name] = column + compaction_strategy = create_compaction_strategy(table_data) + is_compact = (clustering_size != comparator.results.size - 1) || + !comparator.collections + table_options = + create_table_options(table_data, compaction_strategy, is_compact) + + table = Cassandra::Table.new(@schema.keyspace(keyspace_name), + table_name, + partition_key, + clustering_columns, + other_columns, + table_options, + clustering_order, + table_data['id']) + + # Create Index objects and add them to the table. + index_rows.each do |column, row| + create_index(table, column, row) end - other_columns.each do |column| - table_columns[column.name] = column + # Create Trigger objects and add them to the table. + rows_triggers.each do |row_trigger| + table.add_trigger(Cassandra::Trigger.new(table, + row_trigger['trigger_name'], + row_trigger['trigger_options'])) end - compaction_strategy = create_compaction_strategy(table_data) - is_compact = (clustering_size != comparator.results.size - 1) || - !comparator.collections - table_options = - create_table_options(table_data, compaction_strategy, is_compact) + table + end - Table.new(keyspace_name, - table_name, - partition_key, - clustering_columns, - table_columns, - table_options, - clustering_order) + def select_triggers(connection) + send_select_request(connection, SELECT_TRIGGERS) end def select_keyspace(connection, keyspace_name) @@ -581,6 +694,12 @@ def select_keyspace_columns(connection, keyspace_name) send_select_request(connection, SELECT_KEYSPACE_COLUMNS, params, hints) end + def select_keyspace_triggers(connection, keyspace_name) + params = [keyspace_name] + hints = [Types.varchar] + send_select_request(connection, SELECT_KEYSPACE_TRIGGERS, params, hints) + end + def select_table(connection, keyspace_name, table_name) params = [keyspace_name, table_name] hints = [Types.varchar, Types.varchar] @@ -593,13 +712,19 @@ def select_table_columns(connection, keyspace_name, table_name) send_select_request(connection, SELECT_TABLE_COLUMNS, params, hints) end + def select_table_triggers(connection, keyspace_name, table_name) + params = [keyspace_name, table_name] + hints = [Types.varchar, Types.varchar] + send_select_request(connection, SELECT_TABLE_TRIGGERS, params, hints) + end + def create_table_options(table_data, compaction_strategy, is_compact) compression_parameters = ::JSON.load(table_data['compression_parameters']) if compression_parameters['sstable_compression'] - compression_parameters['sstable_compression']. - slice!(COMPRESSION_PACKAGE_PREFIX) + compression_parameters['sstable_compression'] + .slice!(COMPRESSION_PACKAGE_PREFIX) end - Table::Options.new( + Cassandra::ColumnContainer::Options.new( table_data['comment'], table_data['read_repair_chance'], table_data['local_read_repair_chance'], @@ -616,7 +741,10 @@ def create_table_options(table_data, compaction_strategy, is_compact) nil, compaction_strategy, compression_parameters, - is_compact + is_compact, + table_data['crc_check_chance'], + table_data['extensions'], + nil ) end end @@ -624,11 +752,11 @@ def create_table_options(table_data, compaction_strategy, is_compact) class V2_1_x < V2_0_x SELECT_TYPES = 'SELECT * FROM system.schema_usertypes'.freeze SELECT_KEYSPACE_TYPES = - 'SELECT * FROM system.schema_usertypes WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system.schema_usertypes WHERE keyspace_name = ?'.freeze SELECT_TYPE = - 'SELECT * ' \ - 'FROM system.schema_usertypes ' \ - 'WHERE keyspace_name = ? AND type_name = ?'.freeze + 'SELECT * ' \ + 'FROM system.schema_usertypes ' \ + 'WHERE keyspace_name = ? AND type_name = ?'.freeze private @@ -668,10 +796,10 @@ def select_type(connection, keyspace_name, type_name) def create_table_options(table_data, compaction_strategy, is_compact) compression_parameters = ::JSON.load(table_data['compression_parameters']) if compression_parameters['sstable_compression'] - compression_parameters['sstable_compression']. - slice!(COMPRESSION_PACKAGE_PREFIX) + compression_parameters['sstable_compression'] + .slice!(COMPRESSION_PACKAGE_PREFIX) end - Table::Options.new( + Cassandra::ColumnContainer::Options.new( table_data['comment'], table_data['read_repair_chance'], table_data['local_read_repair_chance'], @@ -688,7 +816,10 @@ def create_table_options(table_data, compaction_strategy, is_compact) table_data['max_index_interval'], compaction_strategy, compression_parameters, - is_compact + is_compact, + table_data['crc_check_chance'], + table_data['extensions'], + nil ) end end @@ -697,19 +828,19 @@ class V2_2_x < V2_1_x SELECT_FUNCTIONS = 'SELECT * FROM system.schema_functions'.freeze SELECT_AGGREGATES = 'SELECT * FROM system.schema_aggregates'.freeze SELECT_KEYSPACE_FUNCTIONS = - 'SELECT * FROM system.schema_functions WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system.schema_functions WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_AGGREGATES = - 'SELECT * FROM system.schema_aggregates WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system.schema_aggregates WHERE keyspace_name = ?'.freeze SELECT_FUNCTION = - 'SELECT * ' \ - 'FROM system.schema_functions ' \ - 'WHERE keyspace_name = ? AND function_name = ? ' \ - 'AND argument_types = ?'.freeze + 'SELECT * ' \ + 'FROM system.schema_functions ' \ + 'WHERE keyspace_name = ? AND function_name = ? ' \ + 'AND argument_types = ?'.freeze SELECT_AGGREGATE = - 'SELECT * ' \ - 'FROM system.schema_aggregates ' \ - 'WHERE keyspace_name = ? AND aggregate_name = ? ' \ - 'AND argument_types = ?'.freeze + 'SELECT * ' \ + 'FROM system.schema_aggregates ' \ + 'WHERE keyspace_name = ? AND aggregate_name = ? ' \ + 'AND argument_types = ?'.freeze # parse an array of string argument types and return an array of # [Cassandra::Type]s. @@ -730,41 +861,43 @@ def create_function(function_data) function_name = function_data['function_name'] function_lang = function_data['language'] function_type = - @type_parser.parse(function_data['return_type']).results.first.first + @type_parser.parse(function_data['return_type']).results.first.first function_body = function_data['body'] called_on_null = function_data['called_on_null_input'] arguments = [] - Array(function_data['argument_names']). - zip(Array(function_data['argument_types'])) do |argument_name, fqcn| + Array(function_data['argument_names']) + .zip(Array(function_data['argument_types'])) do |argument_name, fqcn| argument_type = @type_parser.parse(fqcn).results.first.first arguments << Argument.new(argument_name, argument_type) end - Function.new(keyspace_name, - function_name, - function_lang, - function_type, - arguments, - function_body, - called_on_null) + Cassandra::Function.new(keyspace_name, + function_name, + function_lang, + function_type, + arguments, + function_body, + called_on_null) end def create_aggregate(aggregate_data, functions) keyspace_name = aggregate_data['keyspace_name'] aggregate_name = aggregate_data['aggregate_name'] aggregate_type = - @type_parser.parse(aggregate_data['return_type']).results.first.first + @type_parser.parse(aggregate_data['return_type']).results.first.first argument_types = aggregate_data['argument_types'].map do |fqcn| @type_parser.parse(fqcn).results.first.first end.freeze state_type = - @type_parser.parse(aggregate_data['state_type']).results.first.first + @type_parser.parse(aggregate_data['state_type']).results.first.first initial_state = Util.encode_object( - Protocol::Coder.read_value_v4( - Protocol::CqlByteBuffer.new.append_bytes(aggregate_data['initcond']), - state_type)) + Protocol::Coder.read_value_v4( + Protocol::CqlByteBuffer.new.append_bytes(aggregate_data['initcond']), + state_type, nil + ) + ) # The state-function takes arguments: first the stype, then the args of the aggregate. state_function = functions.get(aggregate_data['state_func'], @@ -818,53 +951,73 @@ def select_aggregate(connection, keyspace_name, aggregate_name, aggregate_args) end class V3_0_x < V2_2_x - SELECT_KEYSPACES = "SELECT * FROM system_schema.keyspaces".freeze - SELECT_TABLES = "SELECT * FROM system_schema.tables".freeze - SELECT_COLUMNS = "SELECT * FROM system_schema.columns".freeze - SELECT_TYPES = "SELECT * FROM system_schema.types".freeze - SELECT_FUNCTIONS = "SELECT * FROM system_schema.functions".freeze - SELECT_AGGREGATES = "SELECT * FROM system_schema.aggregates".freeze - SELECT_INDEXES = "SELECT * FROM system_schema.indexes".freeze - SELECT_VIEWS = "SELECT * FROM system_schema.materialized_views".freeze + SELECT_KEYSPACES = 'SELECT * FROM system_schema.keyspaces'.freeze + SELECT_TABLES = 'SELECT * FROM system_schema.tables'.freeze + SELECT_COLUMNS = 'SELECT * FROM system_schema.columns'.freeze + SELECT_TYPES = 'SELECT * FROM system_schema.types'.freeze + SELECT_FUNCTIONS = 'SELECT * FROM system_schema.functions'.freeze + SELECT_AGGREGATES = 'SELECT * FROM system_schema.aggregates'.freeze + SELECT_INDEXES = 'SELECT * FROM system_schema.indexes'.freeze + SELECT_VIEWS = 'SELECT * FROM system_schema.views'.freeze + SELECT_TRIGGERS = 'SELECT * FROM system_schema.triggers'.freeze SELECT_KEYSPACE = - 'SELECT * FROM system_schema.keyspaces WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system_schema.keyspaces WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_TABLES = - 'SELECT * FROM system_schema.tables WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system_schema.tables WHERE keyspace_name = ?'.freeze + SELECT_KEYSPACE_INDEXES = + 'SELECT * FROM system_schema.indexes WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_COLUMNS = - 'SELECT * FROM system_schema.columns WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system_schema.columns WHERE keyspace_name = ?'.freeze + SELECT_KEYSPACE_VIEWS = + 'SELECT * FROM system_schema.views WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_TYPES = - "SELECT * FROM system_schema.types WHERE keyspace_name = ?".freeze + 'SELECT * FROM system_schema.types WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_FUNCTIONS = - 'SELECT * FROM system_schema.functions WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system_schema.functions WHERE keyspace_name = ?'.freeze SELECT_KEYSPACE_AGGREGATES = - 'SELECT * FROM system_schema.aggregates WHERE keyspace_name = ?'.freeze + 'SELECT * FROM system_schema.aggregates WHERE keyspace_name = ?'.freeze + SELECT_KEYSPACE_TRIGGERS = + 'SELECT * FROM system_schema.triggers WHERE keyspace_name = ?'.freeze SELECT_TABLE = - 'SELECT * ' \ - 'FROM system_schema.tables ' \ - 'WHERE keyspace_name = ? AND table_name = ?'.freeze + 'SELECT * ' \ + 'FROM system_schema.tables ' \ + 'WHERE keyspace_name = ? AND table_name = ?'.freeze SELECT_TABLE_COLUMNS = - 'SELECT * ' \ - 'FROM system_schema.columns ' \ - 'WHERE keyspace_name = ? AND table_name = ?'.freeze + 'SELECT * ' \ + 'FROM system_schema.columns ' \ + 'WHERE keyspace_name = ? AND table_name = ?'.freeze + SELECT_TABLE_INDEXES = + 'SELECT * ' \ + 'FROM system_schema.indexes ' \ + 'WHERE keyspace_name = ? AND table_name = ?'.freeze + SELECT_TABLE_TRIGGERS = + 'SELECT * ' \ + 'FROM system_schema.triggers ' \ + 'WHERE keyspace_name = ? AND table_name = ?'.freeze + + SELECT_VIEW = + 'SELECT * ' \ + 'FROM system_schema.views ' \ + 'WHERE keyspace_name = ? AND view_name = ?'.freeze SELECT_TYPE = - 'SELECT * ' \ - 'FROM system_schema.types ' \ - 'WHERE keyspace_name = ? AND type_name = ?'.freeze + 'SELECT * ' \ + 'FROM system_schema.types ' \ + 'WHERE keyspace_name = ? AND type_name = ?'.freeze SELECT_FUNCTION = - 'SELECT * ' \ - 'FROM system_schema.functions ' \ - 'WHERE keyspace_name = ? AND function_name = ? ' \ - 'AND argument_types = ?'.freeze + 'SELECT * ' \ + 'FROM system_schema.functions ' \ + 'WHERE keyspace_name = ? AND function_name = ? ' \ + 'AND argument_types = ?'.freeze SELECT_AGGREGATE = - 'SELECT * ' \ - 'FROM system_schema.aggregates ' \ - 'WHERE keyspace_name = ? AND aggregate_name = ? ' \ - 'AND argument_types = ?'.freeze + 'SELECT * ' \ + 'FROM system_schema.aggregates ' \ + 'WHERE keyspace_name = ? AND aggregate_name = ? ' \ + 'AND argument_types = ?'.freeze # parse an array of string argument types and return an array of # [Cassandra::Type]s. @@ -889,10 +1042,22 @@ def select_tables(connection) send_select_request(connection, SELECT_TABLES) end + def select_indexes(connection) + send_select_request(connection, SELECT_INDEXES) + end + + def select_materialized_views(connection) + send_select_request(connection, SELECT_VIEWS) + end + def select_columns(connection) send_select_request(connection, SELECT_COLUMNS) end + def select_triggers(connection) + send_select_request(connection, SELECT_TRIGGERS) + end + def select_types(connection) send_select_request(connection, SELECT_TYPES) end @@ -923,6 +1088,24 @@ def select_keyspace_columns(connection, keyspace_name) send_select_request(connection, SELECT_KEYSPACE_COLUMNS, params, hints) end + def select_keyspace_indexes(connection, keyspace_name) + params = [keyspace_name] + hints = [Types.varchar] + send_select_request(connection, SELECT_KEYSPACE_INDEXES, params, hints) + end + + def select_keyspace_materialized_views(connection, keyspace_name) + params = [keyspace_name] + hints = [Types.varchar] + send_select_request(connection, SELECT_KEYSPACE_VIEWS, params, hints) + end + + def select_keyspace_triggers(connection, keyspace_name) + params = [keyspace_name] + hints = [Types.varchar] + send_select_request(connection, SELECT_KEYSPACE_TRIGGERS, params, hints) + end + def select_keyspace_types(connection, keyspace_name) params = [keyspace_name] hints = [Types.varchar] @@ -948,11 +1131,32 @@ def select_table(connection, keyspace_name, table_name) end def select_table_columns(connection, keyspace_name, table_name) + # This is identical to the 2.0 impl, but the SELECT_TABLE_COLUMNS query + # is different between the two, so we need two impls. :( + # Also, this method works fine for finding view columns as well. params = [keyspace_name, table_name] hints = [Types.varchar, Types.varchar] send_select_request(connection, SELECT_TABLE_COLUMNS, params, hints) end + def select_table_indexes(connection, keyspace_name, table_name) + params = [keyspace_name, table_name] + hints = [Types.varchar, Types.varchar] + send_select_request(connection, SELECT_TABLE_INDEXES, params, hints) + end + + def select_materialized_view(connection, keyspace_name, view_name) + params = [keyspace_name, view_name] + hints = [Types.varchar, Types.varchar] + send_select_request(connection, SELECT_VIEW, params, hints) + end + + def select_table_triggers(connection, keyspace_name, table_name) + params = [keyspace_name, table_name] + hints = [Types.varchar, Types.varchar] + send_select_request(connection, SELECT_TABLE_TRIGGERS, params, hints) + end + def select_type(connection, keyspace_name, type_name) params = [keyspace_name, type_name] hints = [Types.varchar, Types.varchar] @@ -982,19 +1186,19 @@ def create_function(function_data, types = nil) arguments = [] - function_data['argument_names']. - zip(function_data['argument_types']) do |argument_name, argument_type| + function_data['argument_names'] + .zip(function_data['argument_types']) do |argument_name, argument_type| arguments << Argument.new(argument_name, @type_parser.parse(argument_type, types).first) end - Function.new(keyspace_name, - function_name, - function_lang, - function_type, - arguments, - function_body, - called_on_null) + Cassandra::Function.new(keyspace_name, + function_name, + function_lang, + function_type, + arguments, + function_body, + called_on_null) end def create_aggregate(aggregate_data, functions, types = nil) @@ -1002,7 +1206,7 @@ def create_aggregate(aggregate_data, functions, types = nil) aggregate_name = aggregate_data['aggregate_name'] types ||= @schema.keyspace(keyspace_name).send(:raw_types) aggregate_type = - @type_parser.parse(aggregate_data['return_type'], types).first + @type_parser.parse(aggregate_data['return_type'], types).first argument_types = aggregate_data['argument_types'].map do |argument_type| @type_parser.parse(argument_type, types).first end.freeze @@ -1057,16 +1261,14 @@ def create_types(rows_types, types) break if skipped_rows.empty? - if rows_size == skipped_rows.size - raise "Unable to resolve circular references among UDTs when parsing" - else - rows_types, skipped_rows = skipped_rows, rows_types - end + raise 'Unable to resolve circular references among UDTs when parsing' if rows_size == skipped_rows.size + + rows_types, skipped_rows = skipped_rows, rows_types end end - def create_keyspace(keyspace_data, rows_tables, rows_columns, - rows_types, rows_functions, rows_aggregates) + def create_keyspace(keyspace_data, rows_tables, rows_columns, rows_types, + rows_functions, rows_aggregates, rows_views, rows_indexes, rows_triggers) keyspace_name = keyspace_data['keyspace_name'] replication = create_replication(keyspace_data) @@ -1084,10 +1286,24 @@ def create_keyspace(keyspace_data, rows_tables, rows_columns, aggregates.add_or_update(create_aggregate(row, functions, types)) end + # lookup_columns is a hash of . + # However, views are analogous to tables in this context, so we get + # view columns organized by view-name also. + lookup_columns = map_rows_by(rows_columns, 'table_name') - tables = rows_tables.each_with_object({}) do |row, tables| + lookup_indexes = map_rows_by(rows_indexes, 'table_name') + lookup_triggers = map_rows_by(rows_triggers, 'table_name') + tables = rows_tables.each_with_object({}) do |row, h| table_name = row['table_name'] - tables[table_name] = create_table(row, lookup_columns[table_name], types) + h[table_name] = create_table(row, lookup_columns[table_name], + lookup_indexes[table_name], lookup_triggers[table_name], types) + end + + views = rows_views.each_with_object({}) do |row, h| + view_name = row['view_name'] + h[view_name] = create_materialized_view(row, + lookup_columns[view_name], + types) end Keyspace.new(keyspace_name, @@ -1096,7 +1312,8 @@ def create_keyspace(keyspace_data, rows_tables, rows_columns, tables, types, functions, - aggregates) + aggregates, + views) end def create_replication(keyspace_data) @@ -1110,16 +1327,14 @@ def create_compaction_strategy(table_data) options = table_data['compaction'] klass = options.delete('class') klass.slice!('org.apache.cassandra.db.compaction.') - Table::Compaction.new(klass, options) + ColumnContainer::Compaction.new(klass, options) end def create_table_options(table_data, compaction_strategy, is_compact) compression = table_data['compression'] - if compression['class'] - compression['class'].slice!(COMPRESSION_PACKAGE_PREFIX) - end + compression['class'].slice!(COMPRESSION_PACKAGE_PREFIX) if compression['class'] - Table::Options.new( + Cassandra::ColumnContainer::Options.new( table_data['comment'], table_data['read_repair_chance'], table_data['dclocal_read_repair_chance'], @@ -1136,82 +1351,156 @@ def create_table_options(table_data, compaction_strategy, is_compact) table_data['max_index_interval'], compaction_strategy, compression, - is_compact + is_compact, + table_data['crc_check_chance'], + table_data['extensions'], + table_data['cdc'] ) end def create_column(column_data, types) name = column_data['column_name'] - is_static = (column_data['kind'].to_s.upcase == 'STATIC') + is_static = column_data['kind'].to_s.casecmp('STATIC').zero? order = column_data['clustering_order'] == 'desc' ? :desc : :asc - type, is_frozen = @type_parser.parse(column_data['type'], types) + if column_data['type'][0] == "'" + # This is a custom column type. + type = Types.custom(column_data['type'].slice(1, column_data['type'].length - 2)) + is_frozen = false + else + type, is_frozen = @type_parser.parse(column_data['type'], types) + end - Column.new(name, type, order, nil, is_static, is_frozen) + Column.new(name, type, order, is_static, is_frozen) end - def create_table(table_data, rows_columns, types = nil) + def create_table(table_data, rows_columns, rows_indexes, rows_triggers, types = nil) keyspace_name = table_data['keyspace_name'] table_name = table_data['table_name'] table_flags = table_data['flags'] - table_columns = {} - other_columns = [] - clustering_size = 0 is_dense = table_flags.include?('dense') is_super = table_flags.include?('super') is_compound = table_flags.include?('compound') is_compact = is_super || is_dense || !is_compound + is_static_compact = !is_super && !is_dense && !is_compound + # Separate out partition-key, clustering columns, other columns. partition_key = [] clustering_columns = [] clustering_order = [] - types ||= @schema.keyspace(keyspace_name).send(:raw_types) + other_columns = [] + types ||= @schema.keyspace(keyspace_name).send(:raw_types) rows_columns.each do |row| next if row['column_name'].empty? - column = create_column(row, types) kind = row['kind'].to_s index = row['position'] || 0 + if is_static_compact + if kind.casecmp('CLUSTERING').zero? || kind.casecmp('REGULAR').zero? + # Skip clustering columns in static-compact tables; they are internal to C*. + # Oddly so are regular columns. + next + end + if kind.casecmp('STATIC').zero? + # Coerce static type to regular. + kind = 'REGULAR' + row['kind'] = 'regular' + end + end + + column = create_column(row, types) case kind.upcase when 'PARTITION_KEY' partition_key[index] = column when 'CLUSTERING' clustering_columns[index] = column clustering_order[index] = column.order - - if clustering_size.zero? || index == clustering_size - clustering_size = index + 1 - end else other_columns << column end end - partition_key.each do |column| - table_columns[column.name] = column + # Default the crc_check_chance to 1.0 (Java driver does this, so we + # should, too). + table_data['crc_check_chance'] ||= 1.0 + compaction_strategy = create_compaction_strategy(table_data) + table_options = + create_table_options(table_data, compaction_strategy, is_compact) + + table = Cassandra::Table.new(@schema.keyspace(keyspace_name), + table_name, + partition_key, + clustering_columns, + other_columns, + table_options, + clustering_order, + table_data['id']) + rows_indexes.each do |row| + create_index(table, row) end - clustering_columns.each do |column| - table_columns[column.name] = column + # Create Trigger objects and add them to the table. + rows_triggers.each do |row_trigger| + table.add_trigger(Cassandra::Trigger.new(table, + row_trigger['trigger_name'], + row_trigger['options'])) end - other_columns.each do |column| - table_columns[column.name] = column + table + end + + def create_index(table, row_index) + options = row_index['options'] + table.add_index(Cassandra::Index.new(table, row_index['index_name'], + row_index['kind'].downcase.to_sym, + options['target'], options)) + end + + def create_materialized_view(view_data, rows_columns, types = nil) + keyspace_name = view_data['keyspace_name'] + view_name = view_data['view_name'] + base_table_name = view_data['base_table_name'] + include_all_columns = view_data['include_all_columns'] + where_clause = view_data['where_clause'] + + # Separate out partition key, clustering columns, other columns + partition_key = [] + clustering_columns = [] + other_columns = [] + types ||= @schema.keyspace(keyspace_name).send(:raw_types) + + rows_columns.each do |row| + next if row['column_name'].empty? + + column = create_column(row, types) + kind = row['kind'].to_s + index = row['position'] || 0 + + case kind.upcase + when 'PARTITION_KEY' + partition_key[index] = column + when 'CLUSTERING' + clustering_columns[index] = column + else + other_columns << column + end end - compaction_strategy = create_compaction_strategy(table_data) - table_options = - create_table_options(table_data, compaction_strategy, is_compact) + compaction_strategy = create_compaction_strategy(view_data) + view_options = create_table_options(view_data, compaction_strategy, false) - Table.new(keyspace_name, - table_name, - partition_key, - clustering_columns, - table_columns, - table_options, - clustering_order) + MaterializedView.new(@schema.keyspace(keyspace_name), + view_name, + partition_key, + clustering_columns, + other_columns, + view_options, + include_all_columns, + where_clause, + base_table_name, + view_data['id']) end end @@ -1263,6 +1552,13 @@ def fetch_table(connection, keyspace_name, table_name) return Ione::Future.failed(e) end + def fetch_materialized_view(connection, keyspace_name, view_name) + find_fetcher(connection) + .fetch_materialized_view(connection, keyspace_name, view_name) + rescue => e + return Ione::Future.failed(e) + end + def fetch_type(connection, keyspace_name, type_name) find_fetcher(connection) .fetch_type(connection, keyspace_name, type_name) @@ -1304,21 +1600,21 @@ def find_fetcher(connection) unless host ips = @registry.hosts.map(&:ip) raise Errors::ClientError, - "unable to find release version for current host, " \ + 'unable to find release version for current host, ' \ "connected to #{connection.host}, but cluster contains " \ "#{ips}." end version = host.release_version unless version - raise Errors::ClientError, "unable to determine release " \ + raise Errors::ClientError, 'unable to determine release ' \ "version for host: #{host.inspect}" end @fetchers[version] ||= begin current = @versions.find {|v| v.matches?(version)} unless current - raise Errors::ClientError, "unsupported release version " \ + raise Errors::ClientError, 'unsupported release version ' \ "#{version.inspect}." end current.fetcher diff --git a/lib/cassandra/cluster/schema/fqcn_type_parser.rb b/lib/cassandra/cluster/schema/fqcn_type_parser.rb index 9472c12b0..ac70c2284 100644 --- a/lib/cassandra/cluster/schema/fqcn_type_parser.rb +++ b/lib/cassandra/cluster/schema/fqcn_type_parser.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -109,9 +109,7 @@ def create_type(node) end def lookup_type(node) - if node.name == 'org.apache.cassandra.db.marshal.FrozenType' - return lookup_type(node.children.first) - end + return lookup_type(node.children.first) if node.name == 'org.apache.cassandra.db.marshal.FrozenType' type = @@types.fetch(node.name) do return Cassandra::Types.custom(dump_node(node)) @@ -169,9 +167,7 @@ def parse_node(string) def dump_node(node) str = node.name - unless node.children.empty? - str << '(' + node.children.map { |n| dump_node(n) }.join(',') + ')' - end + str << '(' + node.children.map { |n| dump_node(n) }.join(',') + ')' unless node.children.empty? str end end diff --git a/lib/cassandra/cluster/schema/partitioners.rb b/lib/cassandra/cluster/schema/partitioners.rb index 5c0abf8e8..38ced1b96 100644 --- a/lib/cassandra/cluster/schema/partitioners.rb +++ b/lib/cassandra/cluster/schema/partitioners.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/schema/partitioners/murmur3.rb b/lib/cassandra/cluster/schema/partitioners/murmur3.rb index 6d7ebb783..4146ca82e 100644 --- a/lib/cassandra/cluster/schema/partitioners/murmur3.rb +++ b/lib/cassandra/cluster/schema/partitioners/murmur3.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,11 @@ class Schema module Partitioners # @private class Murmur3 + # @private + LONG_MIN = -2**63 + # @private + LONG_MAX = 2**63 - 1 + def create_token(partition_key) token = Cassandra::Murmur3.hash(partition_key) token = LONG_MAX if token == LONG_MIN @@ -33,13 +38,6 @@ def create_token(partition_key) def parse_token(token_string) token_string.to_i end - - private - - # @private - LONG_MIN = -2**63 - # @private - LONG_MAX = 2**63 - 1 end end end diff --git a/lib/cassandra/cluster/schema/partitioners/ordered.rb b/lib/cassandra/cluster/schema/partitioners/ordered.rb index 611146e2c..4842bcc21 100644 --- a/lib/cassandra/cluster/schema/partitioners/ordered.rb +++ b/lib/cassandra/cluster/schema/partitioners/ordered.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/schema/partitioners/random.rb b/lib/cassandra/cluster/schema/partitioners/random.rb index 0738804a8..960bf4ddc 100644 --- a/lib/cassandra/cluster/schema/partitioners/random.rb +++ b/lib/cassandra/cluster/schema/partitioners/random.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/schema/replication_strategies.rb b/lib/cassandra/cluster/schema/replication_strategies.rb index 49f062470..0b9ee91f7 100644 --- a/lib/cassandra/cluster/schema/replication_strategies.rb +++ b/lib/cassandra/cluster/schema/replication_strategies.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/schema/replication_strategies/network_topology.rb b/lib/cassandra/cluster/schema/replication_strategies/network_topology.rb index dc0e137a2..5a85f6795 100644 --- a/lib/cassandra/cluster/schema/replication_strategies/network_topology.rb +++ b/lib/cassandra/cluster/schema/replication_strategies/network_topology.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/schema/replication_strategies/none.rb b/lib/cassandra/cluster/schema/replication_strategies/none.rb index 7bd0cab2b..0050b7657 100644 --- a/lib/cassandra/cluster/schema/replication_strategies/none.rb +++ b/lib/cassandra/cluster/schema/replication_strategies/none.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/cluster/schema/replication_strategies/simple.rb b/lib/cassandra/cluster/schema/replication_strategies/simple.rb index 0230f4180..b4d88a29f 100644 --- a/lib/cassandra/cluster/schema/replication_strategies/simple.rb +++ b/lib/cassandra/cluster/schema/replication_strategies/simple.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/column.rb b/lib/cassandra/column.rb index b62f1fe5b..b7617ca54 100644 --- a/lib/cassandra/column.rb +++ b/lib/cassandra/column.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,36 +21,18 @@ module Cassandra # @see Cassandra::Table#each_column # @see Cassandra::Table#column class Column - # @private - class Index - # @return [String] index name - attr_reader :name - # @return [String] custom index class name - attr_reader :custom_class_name - - # @private - def initialize(name, custom_class_name = nil) - @name = name - @custom_class_name = custom_class_name - end - end - # @return [String] column name attr_reader :name # @return [Cassandra::Type] column type attr_reader :type # @return [Symbol] column order (`:asc` or `:desc`) attr_reader :order - # @private - # @return [Cassandra::Column::Index, nil] column index - attr_reader :index # @private - def initialize(name, type, order, index = nil, is_static = false, is_frozen = false) + def initialize(name, type, order, is_static = false, is_frozen = false) @name = name @type = type @order = order - @index = index @static = is_static @frozen = is_frozen end @@ -76,7 +58,6 @@ def eql?(other) @name == other.name && @type == other.type && @order == other.order && - @index == other.index && @static == other.static? && @frozen == other.frozen? end diff --git a/lib/cassandra/column_container.rb b/lib/cassandra/column_container.rb new file mode 100644 index 000000000..349b4bd76 --- /dev/null +++ b/lib/cassandra/column_container.rb @@ -0,0 +1,326 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + # This class contains all the logic needed for manipulating columns of an object. + class ColumnContainer + # Encapsulates all of the configuration options of a column-container. + class Options + # @return [String] the comment attribute of this column-container. + attr_reader :comment + # @return [Float] the chance with which a read repair is triggered for this column-container. + attr_reader :read_repair_chance + # @return [Float] the cluster local read repair chance for this column-container. + attr_reader :local_read_repair_chance + # @return [Integer] the tombstone garbage collection grace time in seconds for this column-container. + attr_reader :gc_grace_seconds + # @return [Hash] the caching options for this column-container. + attr_reader :caching + # @return [Float] the false positive chance for the Bloom filter of this column-container. + attr_reader :bloom_filter_fp_chance + # @return [Integer] how often (in milliseconds) to flush the memtable of this column-container. + attr_reader :memtable_flush_period_in_ms + # @return [Integer] the default TTL for this column-container. + attr_reader :default_time_to_live + # Return the speculative retry setting of this column-container, which determines how much + # response delay the coordinator node will tolerate from the chosen replica before + # retrying the request on other replicas. This setting can be expressed as a fixed + # delay in ms (e.g. 10ms) or as a percentile indicating "when the response time has + # exceeded the Nth percentile of read response times for this object" (e.g. 99percentile). + # @return [String] the speculative retry setting of this column-container. + attr_reader :speculative_retry + # Return the index interval of this column-container; Cassandra will hold `1/index_interval` of row keys in + # memory. + # @return [Integer] the index interval of this column-container. May be nil, indicating a default value of 128. + attr_reader :index_interval + # @return [Hash] compression settings + attr_reader :compression + # When compression is enabled, this option defines the probability + # with which checksums for compressed blocks are checked during reads. + # @return [Float] the probability of checking checksums on compressed blocks. + attr_reader :crc_check_chance + # @return [Hash] the extension options of this column-container. + attr_reader :extensions + + # @return [ColumnContainer::Compaction] the compaction strategy of this column-container. + attr_reader :compaction_strategy + + # @return whether or not change data capture is enabled on this table. + attr_reader :cdc + + # @private + def initialize(comment, + read_repair_chance, + local_read_repair_chance, + gc_grace_seconds, + caching, + bloom_filter_fp_chance, + populate_io_cache_on_flush, + memtable_flush_period_in_ms, + default_time_to_live, + speculative_retry, + index_interval, + replicate_on_write, + min_index_interval, + max_index_interval, + compaction_strategy, + compression, + compact_storage, + crc_check_chance, + extensions, + cdc) + @comment = comment + @read_repair_chance = read_repair_chance + @local_read_repair_chance = local_read_repair_chance + @gc_grace_seconds = gc_grace_seconds + @caching = caching + @bloom_filter_fp_chance = bloom_filter_fp_chance + @populate_io_cache_on_flush = populate_io_cache_on_flush + @memtable_flush_period_in_ms = memtable_flush_period_in_ms + @default_time_to_live = default_time_to_live + @speculative_retry = speculative_retry + @index_interval = index_interval + @replicate_on_write = replicate_on_write + @min_index_interval = min_index_interval + @max_index_interval = max_index_interval + @compaction_strategy = compaction_strategy + @compression = compression + @compact_storage = compact_storage + @crc_check_chance = crc_check_chance + @extensions = extensions + @cdc = cdc + end + + # Return whether to replicate counter updates to other replicas. It is *strongly* recommended + # that this setting be `true`. Otherwise, counter updates are only written to one replica + # and fault tolerance is sacrificed. + # @return [Boolean] whether to replicate counter updates to other replicas. + def replicate_on_write? + @replicate_on_write + end + + # @return [Boolean] whether to populate the I/O cache on flush of this + # column-container. May be nil, indicating a default value of `false`. + def populate_io_cache_on_flush? + @populate_io_cache_on_flush + end + + # @return [Boolean] whether this column-container uses compact storage. + def compact_storage? + @compact_storage + end + + # @private + def to_cql + options = [] + + options << 'COMPACT STORAGE' if @compact_storage + unless @bloom_filter_fp_chance.nil? + options << "bloom_filter_fp_chance = #{Util.encode_object(@bloom_filter_fp_chance)}" + end + options << "caching = #{Util.encode_object(@caching)}" unless @caching.nil? + options << 'cdc = true' if @cdc + options << "comment = #{Util.encode_object(@comment)}" unless @comment.nil? + options << "compaction = #{@compaction_strategy.to_cql}" unless @compaction_strategy.nil? + options << "compression = #{Util.encode_object(@compression)}" unless @compression.nil? + options << "crc_check_chance = #{Util.encode_object(@crc_check_chance)}" unless @crc_check_chance.nil? + unless @local_read_repair_chance.nil? + options << "dclocal_read_repair_chance = #{Util.encode_object(@local_read_repair_chance)}" + end + unless @default_time_to_live.nil? + options << "default_time_to_live = #{Util.encode_object(@default_time_to_live)}" + end + options << "gc_grace_seconds = #{Util.encode_object(@gc_grace_seconds)}" unless @gc_grace_seconds.nil? + options << "index_interval = #{Util.encode_object(@index_interval)}" unless @index_interval.nil? + options << "max_index_interval = #{Util.encode_object(@max_index_interval)}" unless @max_index_interval.nil? + unless @memtable_flush_period_in_ms.nil? + options << "memtable_flush_period_in_ms = #{Util.encode_object(@memtable_flush_period_in_ms)}" + end + options << "min_index_interval = #{Util.encode_object(@min_index_interval)}" unless @min_index_interval.nil? + unless @populate_io_cache_on_flush.nil? + options << "populate_io_cache_on_flush = '#{@populate_io_cache_on_flush}'" + end + options << "read_repair_chance = #{Util.encode_object(@read_repair_chance)}" unless @read_repair_chance.nil? + options << "replicate_on_write = '#{@replicate_on_write}'" unless @replicate_on_write.nil? + options << "speculative_retry = #{Util.encode_object(@speculative_retry)}" unless @speculative_retry.nil? + options.join("\nAND ") + end + + # @private + def eql?(other) + other.is_a?(Options) && + @comment == other.comment && + @read_repair_chance == other.read_repair_chance && + @local_read_repair_chance == other.local_read_repair_chance && + @gc_grace_seconds == other.gc_grace_seconds && + @caching == other.caching && + @bloom_filter_fp_chance == other.bloom_filter_fp_chance && + @populate_io_cache_on_flush == other.populate_io_cache_on_flush? && + @memtable_flush_period_in_ms == other.memtable_flush_period_in_ms && + @default_time_to_live == other.default_time_to_live && + @speculative_retry == other.speculative_retry && + @index_interval == other.index_interval && + @replicate_on_write == other.replicate_on_write? && + @compaction_strategy == other.compaction_strategy && + @compression == other.compression && + @compact_storage == other.compact_storage? && + @crc_check_chance == other.crc_check_chance && + @extensions == other.extensions && + @cdc == other.cdc + end + alias == eql? + end + + # Encapsulates the compaction strategy of a column-container. + class Compaction + # @return [String] the name of the Cassandra class that performs compaction. + attr_reader :class_name + # @return [Hash] compaction strategy options + attr_reader :options + + # @private + def initialize(class_name, options) + @class_name = class_name + @options = options + end + + # @private + def to_cql + compaction = {'class' => @class_name} + compaction.merge!(@options) + + Util.encode_hash(compaction) + end + + # @private + def eql?(other) + other.is_a?(Compaction) && + @class_name == other.class_name && + @options == other.options + end + alias == eql? + end + + # @return [String] name of this column-container + attr_reader :name + # @return [Cassandra::Uuid] the id of this object in Cassandra. + attr_reader :id + # @return [Cassandra::Keyspace] the keyspace that this column-container belongs to. + attr_reader :keyspace + # @return [ColumnContainer::Options] collection of configuration options of this column-container. + attr_reader :options + # @return [Array] ordered list of column-names that make up the partition-key. + attr_reader :partition_key + # @return [Array] ordered list of column-names that make up the clustering-columns. + attr_reader :clustering_columns + # @return [Array] primary key of this column-container. It's the combination of + # `partition_key` and `clustering_columns`. + # @note This composition produces a flat list, so it will not be possible for the caller to distinguish + # partition-key columns from clustering-columns. + attr_reader :primary_key + + # @private + def initialize(keyspace, + name, + partition_key, + clustering_columns, + other_columns, + options, + id) + @keyspace = keyspace + @name = name.freeze + @partition_key = partition_key.freeze + @clustering_columns = clustering_columns.freeze + @options = options + @id = id + + # Make one array of all the columns, ordered with partition key, clustering + # columns, then other columns. Make a hash as well, to support random access + # to column metadata for a given column name. Save off the primary key (which + # is partition-key + clustering-columns) while we're at it. + + @primary_key = @partition_key.dup.concat(@clustering_columns).freeze + @columns = @primary_key.dup.concat(other_columns).freeze + @columns_hash = @columns.each_with_object({}) do |col, h| + h[col.name] = col + end + end + + # @param name [String] column name + # @return [Boolean] whether this column-container has a given column + def has_column?(name) + @columns_hash.key?(name) + end + + # @param name [String] column name + # @return [Cassandra::Column, nil] a column or nil + def column(name) + @columns_hash[name] + end + + # Yield or enumerate each column defined in this column-container + # @overload each_column + # @yieldparam column [Cassandra::Column] current column + # @return [Cassandra::ColumnContainer] self + # @overload each_column + # @return [Array] a list of columns + def each_column(&block) + if block_given? + @columns.each(&block) + self + else + @columns + end + end + alias columns each_column + + # @private + # keyspace attribute may be nil because when this object was constructed, we didn't have + # its keyspace constructed yet. So allow updating @keyspace, thus + # allowing fetchers to create keyspace, table/view, and hook them together without + # worrying about chickens and eggs. + # rubocop:disable Style/AccessorMethodName + def set_keyspace(keyspace) + @keyspace = keyspace + end + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "@keyspace=#{@keyspace.name} @name=#{@name}>" + end + + # @private + def eql?(other) + other.is_a?(ColumnContainer) && + @keyspace == other.keyspace && + @name == other.name && + @partition_key == other.partition_key && + @clustering_columns == other.clustering_columns && + @columns == other.raw_columns && + @options == other.options + end + alias == eql? + + # @private + def raw_columns + @columns + end + protected :raw_columns + end +end diff --git a/lib/cassandra/compression.rb b/lib/cassandra/compression.rb index 4de7d6890..028ded01f 100644 --- a/lib/cassandra/compression.rb +++ b/lib/cassandra/compression.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/compression/compressors/lz4.rb b/lib/cassandra/compression/compressors/lz4.rb index 3d67c929a..dd80f2a63 100644 --- a/lib/cassandra/compression/compressors/lz4.rb +++ b/lib/cassandra/compression/compressors/lz4.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,6 +28,9 @@ module Compressors # :lz4` option when calling {Cassandra.cluster} and one will be created # automatically for you. class Lz4 < Compressor + # @private + BUFFER_FORMAT = 'Na*'.freeze + # @return [String] `'lz4'` attr_reader :algorithm @@ -64,11 +67,6 @@ def decompress(str) decompressed_size, compressed_data = str.to_s.unpack(BUFFER_FORMAT) ::LZ4::Raw.decompress(compressed_data, decompressed_size).first end - - private - - # @private - BUFFER_FORMAT = 'Na*'.freeze end end end diff --git a/lib/cassandra/compression/compressors/snappy.rb b/lib/cassandra/compression/compressors/snappy.rb index 4d12db419..dd170413b 100644 --- a/lib/cassandra/compression/compressors/snappy.rb +++ b/lib/cassandra/compression/compressors/snappy.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/custom_data.rb b/lib/cassandra/custom_data.rb new file mode 100644 index 000000000..68492b5d5 --- /dev/null +++ b/lib/cassandra/custom_data.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +# Use this module to mark domain object classes as custom type implementations for custom-type +# columns in C*. This module has no logic of its own, but indicates that the marked class has +# certain methods. +# @private +module Cassandra + module CustomData + def self.included(base) + base.send :include, InstanceMethods + base.extend ClassMethods + end + + module ClassMethods + # @return [Cassandra::Types::Custom] the custom type that this class represents. + def type + raise NotImplementedError, "#{self.class} must implement the :type class method" + end + + # Deserialize the given data into an instance of this domain object class. + # @param data [String] byte-array representation of a column value of this custom type. + # @return An instance of the domain object class. + # @raise [Cassandra::Errors::DecodingError] upon failure. + def deserialize(data) + raise NotImplementedError, "#{self.class} must implement the :deserialize class method" + end + end + + module InstanceMethods + # Serialize this domain object into a byte array to send to C*. + # @return [String] byte-array representation of this domain object. + def serialize + raise NotImplementedError, "#{self.class} must implement the :serialize instance method" + end + end + end +end diff --git a/lib/cassandra/driver.rb b/lib/cassandra/driver.rb index e3b6589e5..3e8f2b8fe 100644 --- a/lib/cassandra/driver.rb +++ b/lib/cassandra/driver.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ def self.let(name, &block) cluster_registry, cluster_schema, cluster_metadata, - load_balancing_policy, + profile_manager.default_profile.load_balancing_policy, reconnection_policy, address_resolution_policy, connector, @@ -92,30 +92,34 @@ def self.let(name, &block) schema_fetcher) end + let(:cluster_klass) { Cluster } + let(:cluster) do - Cluster.new(logger, - io_reactor, - executor, - control_connection, - cluster_registry, - cluster_schema, - cluster_metadata, - execution_options, - connection_options, - load_balancing_policy, - reconnection_policy, - retry_policy, - address_resolution_policy, - connector, - futures_factory) + cluster_klass.new(logger, + io_reactor, + executor, + control_connection, + cluster_registry, + cluster_schema, + cluster_metadata, + execution_options, + connection_options, + profile_manager, + reconnection_policy, + address_resolution_policy, + connector, + futures_factory, + timestamp_generator) end let(:execution_options) do - Execution::Options.new(consistency: consistency, + Execution::Options.new(consistency: profile_manager.default_profile.consistency, trace: trace, page_size: page_size, - timeout: timeout, - idempotent: false) + timeout: profile_manager.default_profile.timeout, + idempotent: false, + load_balancing_policy: profile_manager.default_profile.load_balancing_policy, + retry_policy: profile_manager.default_profile.retry_policy) end let(:connection_options) do @@ -135,14 +139,17 @@ def self.let(name, &block) synchronize_schema, schema_refresh_delay, schema_refresh_timeout, - client_timestamps, nodelay, - requests_per_connection + requests_per_connection, + custom_types, + allow_beta_protocol ) end + let(:custom_types) { [] } let(:port) { 9042 } - let(:protocol_version) { 4 } + let(:protocol_version) { nil } + let(:allow_beta_protocol) { false } let(:connect_timeout) { 10 } let(:ssl) { false } let(:logger) { NullLogger.new } @@ -153,7 +160,8 @@ def self.let(name, &block) let(:load_balancing_policy) do LoadBalancing::Policies::TokenAware.new( LoadBalancing::Policies::DCAwareRoundRobin.new(datacenter, 0), - shuffle_replicas) + shuffle_replicas + ) end let(:reconnection_policy) do Reconnection::Policies::Exponential.new(0.5, 30, 2) @@ -165,19 +173,25 @@ def self.let(name, &block) let(:page_size) { 10000 } let(:heartbeat_interval) { 30 } let(:idle_timeout) { 60 } - let(:timeout) { 10 } + let(:timeout) { 12 } let(:synchronize_schema) { true } let(:schema_refresh_delay) { 1 } let(:schema_refresh_timeout) { 10 } let(:thread_pool_size) { 4 } let(:shuffle_replicas) { true } - let(:client_timestamps) { false } let(:nodelay) { true } - + let(:timestamp_generator) { nil } let(:connections_per_local_node) { nil } let(:connections_per_remote_node) { nil } let(:requests_per_connection) { nil } - + let(:default_execution_profile) { + Cassandra::Execution::Profile.new(load_balancing_policy: load_balancing_policy, + retry_policy: retry_policy, + consistency: consistency, + timeout: timeout) + } + let(:execution_profiles) { {} } + let(:profile_manager) { Cassandra::Execution::ProfileManager.new(default_execution_profile, execution_profiles) } let(:listeners) { [] } def initialize(defaults = {}) @@ -185,8 +199,11 @@ def initialize(defaults = {}) end def connect(addresses) - load_balancing_policy.setup(cluster) - cluster_registry.add_listener(load_balancing_policy) + profile_manager.load_balancing_policies.each do |lbp| + lbp.setup(cluster) + cluster_registry.add_listener(lbp) + end + cluster_registry.add_listener(control_connection) listeners.each do |listener| cluster.register(listener) @@ -233,6 +250,9 @@ def create_schema_fetcher_picker picker.when('3.') do Cluster::Schema::Fetchers::V3_0_x.new(schema_cql_type_parser, cluster_schema) end + picker.when('4.') do + Cluster::Schema::Fetchers::V3_0_x.new(schema_cql_type_parser, cluster_schema) + end picker end diff --git a/lib/cassandra/errors.rb b/lib/cassandra/errors.rb index efb9a3a54..2a6ec05d3 100644 --- a/lib/cassandra/errors.rb +++ b/lib/cassandra/errors.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -43,7 +43,8 @@ module HostError # Raised when something unexpected happened. This indicates a server-side # bug. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L654-L655 Description of Server Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L654-L655 Description + # of Server Error in Apache Cassandra native protocol spec v1 class ServerError < ::StandardError include Error, HostError @@ -155,7 +156,8 @@ def execution_info # @note This error can be handled by a {Cassandra::Retry::Policy} to # determine the desired outcome. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L662-L672 Description of Unavailable Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L662-L672 Description + # of Unavailable Error in Apache Cassandra native protocol spec v1 class UnavailableError < ::StandardError include ExecutionError # Consistency level that triggered the error. @@ -202,7 +204,8 @@ def initialize(message, # Raised when the request cannot be processed because the coordinator node # is overloaded # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L673-L674 Description of Overloaded Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L673-L674 Description + # of Overloaded Error in Apache Cassandra native protocol spec v1 class OverloadedError < ::StandardError include ExecutionError, HostError end @@ -210,21 +213,24 @@ class OverloadedError < ::StandardError # Raise when the request was a read request but the coordinator node is # bootstrapping # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L675-L676 Description of Is Bootstrapping Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L675-L676 Description + # of Is Bootstrapping Error in Apache Cassandra native protocol spec v1 class IsBootstrappingError < ::StandardError include ExecutionError, HostError end # Raised when truncation failed. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L677 Description of Truncate Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L677 Description of + # Truncate Error in Apache Cassandra native protocol spec v1 class TruncateError < ::StandardError include ExecutionError end # Raised when a write request timed out. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L678-L703 Description of Write Timeout Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L678-L703 Description + # of Write Timeout Error in Apache Cassandra native protocol spec v1 class WriteTimeoutError < ::StandardError include ExecutionError @@ -273,13 +279,16 @@ def initialize(message, # Raised when a read request timed out. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L704-L721 Description of Read Timeout Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L704-L721 Description + # of Read Timeout Error in Apache Cassandra native protocol spec v1 class ReadTimeoutError < ::StandardError include ExecutionError # @return [Boolean] whether actual data (as opposed to data checksum) was # present in the received responses. attr_reader :retrieved + alias retrieved? retrieved + # @return [Symbol] the original consistency level for the request, one of # {Cassandra::CONSISTENCIES} attr_reader :consistency @@ -318,15 +327,12 @@ def initialize(message, @required = required @received = received end - - def retrieved? - @retrieved - end end # Raised when a write request fails. # - # @see https://github.com/apache/cassandra/blob/33f1edcce97779c971d4f78712a9a8bf014ffbbc/doc/native_protocol_v4.spec#L1111-L1138 Description of Write Failure Error in Apache Cassandra native protocol spec v4 + # @see https://github.com/apache/cassandra/blob/cassandra-3.4/doc/native_protocol_v4.spec#L1106-L1134 Description + # of Write Failure Error in Apache Cassandra native protocol spec v4 class WriteError < ::StandardError include ExecutionError @@ -342,6 +348,9 @@ class WriteError < ::StandardError attr_reader :received # @return [Integer] the number of writes failed attr_reader :failed + # @return [Hash] map of . This is new in v5 and is nil in previous versions + # of the Casssandra protocol. + attr_reader :failures_by_node # @private def initialize(message, @@ -357,7 +366,8 @@ def initialize(message, consistency, required, failed, - received) + received, + failures_by_node) super(message, payload, warnings, @@ -372,12 +382,14 @@ def initialize(message, @required = required @failed = failed @received = received + @failures_by_node = failures_by_node end end # Raised when a read request fails. # - # @see https://github.com/apache/cassandra/blob/33f1edcce97779c971d4f78712a9a8bf014ffbbc/doc/native_protocol_v4.spec#L1089-L1103 Description of Read Failure Error in Apache Cassandra native protocol spec v4 + # @see https://github.com/apache/cassandra/blob/cassandra-3.4/doc/native_protocol_v4.spec#L1084-L1098 Description + # of Read Failure Error in Apache Cassandra native protocol spec v4 class ReadError < ::StandardError include ExecutionError @@ -393,6 +405,9 @@ class ReadError < ::StandardError attr_reader :received # @return [Integer] the number of reads failed attr_reader :failed + # @return [Hash] map of . This is new in v5 and is nil in previous versions + # of the Casssandra protocol. + attr_reader :failures_by_node # @private def initialize(message, @@ -408,7 +423,8 @@ def initialize(message, consistency, required, failed, - received) + received, + failures_by_node) super(message, payload, warnings, @@ -423,6 +439,7 @@ def initialize(message, @required = required @failed = failed @received = received + @failures_by_node = failures_by_node end def retrieved? @@ -432,7 +449,8 @@ def retrieved? # Raised when function execution fails. # - # @see https://github.com/apache/cassandra/blob/33f1edcce97779c971d4f78712a9a8bf014ffbbc/doc/native_protocol_v4.spec#L1104-L1110 Description of Function Failure Error in Apache Cassandra native protocol spec v4 + # @see https://github.com/apache/cassandra/blob/cassandra-3.4/doc/native_protocol_v4.spec#L1099-L1105 Description + # of Function Failure Error in Apache Cassandra native protocol spec v4 class FunctionCallError < ::StandardError include ExecutionError @@ -479,7 +497,8 @@ class ClientError < ::StandardError # Raised when some client message triggered a protocol violation (for # instance a QUERY message is sent before a STARTUP one has been sent) # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L656-L658 Description of Protocol Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L656-L658 Description + # of Protocol Error in Apache Cassandra native protocol spec v1 class ProtocolError < ClientError # @private def initialize(message, @@ -519,7 +538,8 @@ def execution_info # Raised when cannot authenticate to Cassandra # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L659-L660 Description of Bad Credentials Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L659-L660 Description + # of Bad Credentials Error in Apache Cassandra native protocol spec v1 class AuthenticationError < ClientError # @private def initialize(message, @@ -603,7 +623,8 @@ def execution_info # @note Seeing this error can be considered a Ruby Driver bug as it should # handle automatic re-preparing internally. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L738-L741 Description of Unprepared Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L738-L741 Description + # of Unprepared Error in Apache Cassandra native protocol spec v1 class UnpreparedError < ::StandardError include ValidationError # @return [String] prepared statement id that triggered the error @@ -635,14 +656,16 @@ def initialize(message, # Raised when the submitted query has a syntax error. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L723 Description of Syntax Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L723 Description of + # Syntax Error in Apache Cassandra native protocol spec v1 class SyntaxError < ::StandardError include ValidationError end # Raised when the logged user doesn't have the right to perform the query. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L724-L725 Description of Unauthorized Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L724-L725 Description + # of Unauthorized Error in Apache Cassandra native protocol spec v1 class UnauthorizedError < ::StandardError include ValidationError end @@ -655,7 +678,8 @@ class UnauthorizedError < ::StandardError # rescue Cassandra::Errors::InvalidError # end # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L726 Description of Invalid Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L726 Description + # of Invalid Error in Apache Cassandra native protocol spec v1 class InvalidError < ::StandardError include ValidationError end @@ -668,7 +692,8 @@ class InvalidError < ::StandardError # rescue Cassandra::Errors::ConfigurationError # end # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L727 Description of Config Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L727 Description of + # Config Error in Apache Cassandra native protocol spec v1 class ConfigurationError < ::StandardError include ValidationError end @@ -685,7 +710,8 @@ class ConfigurationError < ::StandardError # p ['already exists', e.keyspace, e.table] # end # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L728-L737 Description of Already Exists Error in Apache Cassandra native protocol spec v1 + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v1.spec#L728-L737 Description + # of Already Exists Error in Apache Cassandra native protocol spec v1 class AlreadyExistsError < ConfigurationError # @return [String] keyspace attr_reader :keyspace diff --git a/lib/cassandra/execution/info.rb b/lib/cassandra/execution/info.rb index b735eeb33..a1d4f81d0 100644 --- a/lib/cassandra/execution/info.rb +++ b/lib/cassandra/execution/info.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/execution/options.rb b/lib/cassandra/execution/options.rb index b9ee0435c..50e838738 100644 --- a/lib/cassandra/execution/options.rb +++ b/lib/cassandra/execution/options.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -50,10 +50,12 @@ class Options attr_reader :paging_state # @return [nil, Hash] custom outgoing payload, a map of - # string and byte buffers. + # string and byte buffers. # - # @see https://github.com/apache/cassandra/blob/33f1edcce97779c971d4f78712a9a8bf014ffbbc/doc/native_protocol_v4.spec#L127-L133 Description of custom payload in Cassandra native protocol v4. - # @see https://datastax.github.io/java-driver/manual/custom_payloads/#enabling-custom-payloads-on-c-nodes Enabling custom payloads on Cassandra nodes. + # @see https://github.com/apache/cassandra/blob/cassandra-3.4/doc/native_protocol_v4.spec#L125-L131 Description + # of custom payload in Cassandra native protocol v4. + # @see http://dsdocs30_java/manual/custom_payloads/#enabling-custom-payloads-on-c-nodes + # Enabling custom payloads on Cassandra nodes. # # @example Sending a custom payload # result = session.execute(payload: { @@ -62,21 +64,31 @@ class Options # }) attr_reader :payload + # @return [Cassandra::LoadBalancing::Policy] load-balancing policy that determines which node will run the + # next statement. + attr_reader :load_balancing_policy + + # @return [Cassandra::Retry::Policy] retry policy that determines how request retries should be handled for + # different failure modes. + attr_reader :retry_policy + # @private # @param options [Hash] execution options to validate and encapsulate # @param trusted_options [Options] (optional) base Execution::Options from which # to create this new Options object. def initialize(options, trusted_options = nil) - consistency = options[:consistency] - page_size = options[:page_size] - trace = options[:trace] - timeout = options[:timeout] - serial_consistency = options[:serial_consistency] - paging_state = options[:paging_state] - arguments = options[:arguments] - type_hints = options[:type_hints] - idempotent = options[:idempotent] - payload = options[:payload] + consistency = options[:consistency] + page_size = options[:page_size] + trace = options[:trace] + timeout = options[:timeout] + serial_consistency = options[:serial_consistency] + paging_state = options[:paging_state] + arguments = options[:arguments] + type_hints = options[:type_hints] + idempotent = options[:idempotent] + payload = options[:payload] + load_balancing_policy = options[:load_balancing_policy] + retry_policy = options[:retry_policy] # consistency is a required attribute of an Options object. If we are creating # an Options object from scratch (e.g. no trusted_options as base) validate the @@ -89,6 +101,22 @@ def initialize(options, trusted_options = nil) end end + # load_balancing_policy and retry_policy are required, but can fallback to trusted_options, just like + # consistency. + if trusted_options.nil? || !load_balancing_policy.nil? + methods = [:host_up, :host_down, :host_found, :host_lost, :setup, :teardown, + :distance, :plan] + Util.assert_responds_to_all(methods, load_balancing_policy, + ":load_balancing_policy #{load_balancing_policy.inspect} must respond " \ + "to #{methods.inspect}, but doesn't") + end + if trusted_options.nil? || !retry_policy.nil? + methods = [:read_timeout, :write_timeout, :unavailable] + Util.assert_responds_to_all(methods, retry_policy, + ":retry_policy #{retry_policy.inspect} must respond to #{methods.inspect}, " \ + "but doesn't") + end + unless serial_consistency.nil? Util.assert_one_of(SERIAL_CONSISTENCIES, serial_consistency) do ":serial_consistency must be one of #{SERIAL_CONSISTENCIES.inspect}, " \ @@ -171,14 +199,18 @@ def initialize(options, trusted_options = nil) @serial_consistency = serial_consistency @arguments = arguments @type_hints = type_hints + @load_balancing_policy = load_balancing_policy + @retry_policy = retry_policy else @consistency = consistency || trusted_options.consistency @page_size = page_size || trusted_options.page_size @trace = trace.nil? ? trusted_options.trace? : !!trace - @timeout = timeout || trusted_options.timeout + @timeout = options.key?(:timeout) ? timeout : trusted_options.timeout @serial_consistency = serial_consistency || trusted_options.serial_consistency @arguments = arguments || trusted_options.arguments @type_hints = type_hints || trusted_options.type_hints + @load_balancing_policy = load_balancing_policy || trusted_options.load_balancing_policy + @retry_policy = retry_policy || trusted_options.retry_policy end # The following fields are *not* inherited from trusted_options, so we always @@ -208,18 +240,24 @@ def eql?(other) other.serial_consistency == @serial_consistency && other.paging_state == @paging_state && other.arguments == @arguments && - other.type_hints == @type_hints + other.type_hints == @type_hints && + other.load_balancing_policy == @load_balancing_policy && + other.retry_policy == @retry_policy end alias == eql? # @private def override(*options) merged = options.unshift({}).inject do |base, opts| + # Skip nil args next base unless opts - Util.assert_instance_of(::Hash, opts) do - "options must be a Hash, #{options.inspect} given" + + if opts.is_a?(Cassandra::Execution::Profile) + base.merge!(opts.to_h) + else + Util.assert_instance_of(::Hash, opts, "options must be a Hash, #{opts.inspect} given") + base.merge!(opts) end - base.merge!(opts) end Options.new(merged, self) diff --git a/lib/cassandra/execution/profile.rb b/lib/cassandra/execution/profile.rb new file mode 100644 index 000000000..bc7767a94 --- /dev/null +++ b/lib/cassandra/execution/profile.rb @@ -0,0 +1,150 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module Execution + # A profile is a collection of settings to use when executing and preparing statements. Register different + # profiles when creating the {Cassandra::Cluster} and execute/prepare statements with a particular profile + # by providing its name to the relevant method in {Session}. + # + # @see Cassandra.cluster + # @see Session#execute_async + # @see Session#execute + # @see Session#prepare_async + # @see Session#prepare + class Profile + # @return [Cassandra::LoadBalancing::Policy] load-balancing policy that determines which node will run the + # next statement. + attr_reader :load_balancing_policy + + # @return [Cassandra::Retry::Policy] retry policy that determines how request retries should be handled for + # different failure modes. + attr_reader :retry_policy + + # @return [Symbol] consistency level with which to run statements. + attr_reader :consistency + + # @return [Numeric] request execution timeout in seconds. `nil` means there is no timeout. + attr_reader :timeout + + # @private + DEFAULT_OPTIONS = { consistency: :local_one, timeout: 12 }.freeze + + # @param options [Hash] hash of attributes. Unspecified attributes fall back to system defaults. + # @option options [Numeric] :timeout (12) Request execution timeout in + # seconds. Setting value to `nil` will remove request timeout. + # @option options [Cassandra::LoadBalancing::Policy] :load_balancing_policy ( + # LoadBalancing::Policies::TokenAware(LoadBalancing::Policies::DCAwareRoundRobin)) Load-balancing policy + # that determines which node will run the next statement. + # @option options [Cassandra::Retry::Policy] :retry_policy (Retry::Policies::Default) Retry policy that + # determines how request retries should be handled for different failure modes. + # @option options [Symbol] :consistency (:local_one) Consistency level with which to run statements. Must be one + # of {Cassandra::CONSISTENCIES}. + def initialize(options = {}) + validate(options) + options = DEFAULT_OPTIONS.merge(options) + @load_balancing_policy = options[:load_balancing_policy] || + LoadBalancing::Policies::TokenAware.new( + LoadBalancing::Policies::DCAwareRoundRobin.new, + true) + @retry_policy = options[:retry_policy] || Retry::Policies::Default.new + @consistency = options[:consistency] + @timeout = options[:timeout] + end + + # @private + def to_h + { + load_balancing_policy: @load_balancing_policy, + retry_policy: @retry_policy, + consistency: @consistency, + timeout: @timeout + } + end + + # @private + def eql?(other) + other.is_a?(Profile) && \ + @load_balancing_policy == other.load_balancing_policy && \ + @retry_policy == other.retry_policy && \ + @consistency == other.consistency && \ + @timeout == other.timeout + end + alias == eql? + + # @private + def hash + @hash ||= begin + h = 17 + h = 31 * h + @load_balancing_policy.hash + h = 31 * h + @retry_policy.hash + h = 31 * h + @consistency.hash + h = 31 * h + @timeout.hash + h + end + end + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "load_balancing_policy=#{@load_balancing_policy.inspect}, " \ + "retry_policy=#{@retry_policy.inspect}, " \ + "consistency=#{@consistency.inspect}, " \ + "timeout=#{@timeout.inspect}>" + end + + # @private + def validate(options) + if options.key?(:timeout) + timeout = options[:timeout] + + unless timeout.nil? + Util.assert_instance_of(::Numeric, timeout, ":timeout must be a number of seconds, #{timeout.inspect} given") + Util.assert(timeout > 0, ":timeout must be greater than 0, #{timeout} given") + end + end + + if options.key?(:load_balancing_policy) + load_balancing_policy = options[:load_balancing_policy] + methods = [:host_up, :host_down, :host_found, :host_lost, :setup, :teardown, + :distance, :plan] + Util.assert_responds_to_all(methods, load_balancing_policy) do + ":load_balancing_policy #{load_balancing_policy.inspect} must respond " \ + "to #{methods.inspect}, but doesn't" + end + end + + if options.key?(:retry_policy) + retry_policy = options[:retry_policy] + methods = [:read_timeout, :write_timeout, :unavailable] + Util.assert_responds_to_all(methods, retry_policy) do + ":retry_policy #{retry_policy.inspect} must respond to #{methods.inspect}, " \ + "but doesn't" + end + end + + if options.key?(:consistency) + consistency = options[:consistency] + Util.assert_one_of(CONSISTENCIES, consistency, + ":consistency must be one of #{CONSISTENCIES.inspect}, " \ + "#{consistency.inspect} given") + end + end + end + end +end diff --git a/lib/cassandra/execution/profile_manager.rb b/lib/cassandra/execution/profile_manager.rb new file mode 100644 index 000000000..fdc2d700e --- /dev/null +++ b/lib/cassandra/execution/profile_manager.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module Execution + # @private + class ProfileManager + attr_reader :profiles + attr_reader :load_balancing_policies + + def initialize(default_profile, profiles) + # default_profile is the default profile that we constructed internally. However, the user can override it + # with their own :default profile. If that happens, ignore the internally generated default profile. + + profiles[:default] = default_profile unless profiles.key?(:default) + + # Save off all of the load-balancing policies for easy access. + @load_balancing_policies = Set.new + profiles.each do |name, profile| + @load_balancing_policies << profile.load_balancing_policy + end + @profiles = profiles + end + + def default_profile + @profiles[:default] + end + + def distance(host) + # Return the min distance to the host, as per each lbp. + distances = Set.new + @load_balancing_policies.each do |lbp| + distances.add(lbp.distance(host)) + end + return :local if distances.include?(:local) + return :remote if distances.include?(:remote) + + # Fall back to ignore the host. + :ignore + end + + # NOTE: It's only safe to call add_profile when setting up the cluster object. In particular, + # this is only ok before calling Driver#connect. + def add_profile(name, profile) + @profiles[name] = profile + @load_balancing_policies << profile.load_balancing_policy + end + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "profiles=#{@profiles.inspect}>" + end + end + end +end diff --git a/lib/cassandra/execution/trace.rb b/lib/cassandra/execution/trace.rb index 84cc9d27a..108f46518 100644 --- a/lib/cassandra/execution/trace.rb +++ b/lib/cassandra/execution/trace.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ def ==(other) attr_reader :id # @private - def initialize(id, client) + def initialize(id, client, load_balancing_policy) @id = id @client = client @coordinator = nil @@ -65,6 +65,7 @@ def initialize(id, client) @client_ip = nil @loaded = false @loaded_events = false + @load_balancing_policy = load_balancing_policy mon_initialize end @@ -141,12 +142,12 @@ def load attempt = 1 data = @client.query(Statements::Simple.new(SELECT_SESSION % @id), - VOID_OPTIONS).get.first + VOID_OPTIONS.override(load_balancing_policy: @load_balancing_policy)).get.first while data.nil? && attempt <= 5 sleep(attempt * 0.4) data = @client.query(Statements::Simple.new(SELECT_SESSION % @id), - VOID_OPTIONS).get.first + VOID_OPTIONS.override(load_balancing_policy: @load_balancing_policy)).get.first break if data attempt += 1 end @@ -173,7 +174,7 @@ def load_events @events = [] @client.query(Statements::Simple.new(SELECT_EVENTS % @id), - VOID_OPTIONS).get.each do |row| + VOID_OPTIONS.override(load_balancing_policy: @load_balancing_policy)).get.each do |row| @events << Event.new(row['event_id'], row['activity'], row['source'], diff --git a/lib/cassandra/executors.rb b/lib/cassandra/executors.rb index 66d2f3764..f8debda7c 100644 --- a/lib/cassandra/executors.rb +++ b/lib/cassandra/executors.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -43,8 +43,8 @@ def initialize(size) @cond = new_cond @tasks = ::Array.new @waiting = 0 - @pool = ::Array.new(size, &method(:spawn_thread)) @term = false + @pool = ::Array.new(size, &method(:spawn_thread)) end def execute(*args, &block) diff --git a/lib/cassandra/function.rb b/lib/cassandra/function.rb index a72668a93..949126286 100644 --- a/lib/cassandra/function.rb +++ b/lib/cassandra/function.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/function_collection.rb b/lib/cassandra/function_collection.rb index 4702e65e8..ce872a39d 100644 --- a/lib/cassandra/function_collection.rb +++ b/lib/cassandra/function_collection.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/future.rb b/lib/cassandra/future.rb index d52fea57a..956a150ad 100644 --- a/lib/cassandra/future.rb +++ b/lib/cassandra/future.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -46,9 +46,7 @@ def failure(error) # @private class Error < Future def initialize(error) - unless error.is_a?(::Exception) - raise ::ArgumentError, "error must be an exception, #{error.inspect} given" - end + raise ::ArgumentError, "error must be an exception, #{error.inspect} given" unless error.is_a?(::Exception) @error = error end @@ -519,9 +517,7 @@ def initialize(executor) end def failure(error) - unless error.is_a?(::Exception) - raise ::ArgumentError, "error must be an exception, #{error.inspect} given" - end + raise ::ArgumentError, "error must be an exception, #{error.inspect} given" unless error.is_a?(::Exception) return unless @state == :pending @@ -600,9 +596,7 @@ def get(timeout = nil) timeout &&= Float(timeout) if timeout - if timeout < 0 - raise ::ArgumentError, "timeout cannot be negative, #{timeout.inspect} given" - end + raise ::ArgumentError, "timeout cannot be negative, #{timeout.inspect} given" if timeout < 0 start = ::Time.now now = start diff --git a/lib/cassandra/host.rb b/lib/cassandra/host.rb index daaecaebf..8b04ce88b 100644 --- a/lib/cassandra/host.rb +++ b/lib/cassandra/host.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ module Cassandra class Host - # @return [IPAddr] host ip + # @return [IPAddr] host ip that clients use to connect to this host. attr_reader :ip # @note Host id can be `nil` before cluster has connected. # @return [Cassandra::Uuid, nil] host id. @@ -37,6 +37,14 @@ class Host attr_reader :tokens # @return [Symbol] host status. Must be `:up` or `:down` attr_reader :status + # @note This is the public IP address of the host if the cluster is deployed across multiple Amazon EC2 regions + # (or equivalently multiple networks). Cassandra nodes in other EC2 regions use this address to connect to this + # host. + # @return [IPAddr, String] broadcast address, if available. + attr_reader :broadcast_address + # @note This is the address that other Cassandra nodes use to connect to this host. + # @return [IPAddr, String] listen address, if available. + attr_reader :listen_address # @private def initialize(ip, @@ -45,7 +53,9 @@ def initialize(ip, datacenter = nil, release_version = nil, tokens = EMPTY_LIST, - status = :up) + status = :up, + broadcast_address = nil, + listen_address = nil) @ip = ip @id = id @rack = rack @@ -53,6 +63,10 @@ def initialize(ip, @release_version = release_version @tokens = tokens @status = status + @broadcast_address = broadcast_address.is_a?(String) ? + ::IPAddr.new(broadcast_address) : broadcast_address + @listen_address = listen_address.is_a?(String) ? + ::IPAddr.new(listen_address) : listen_address end # @return [Boolean] whether this host's status is `:up` diff --git a/lib/cassandra/index.rb b/lib/cassandra/index.rb new file mode 100644 index 000000000..f1eaa32dc --- /dev/null +++ b/lib/cassandra/index.rb @@ -0,0 +1,118 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + # Represents an index on a cassandra table + class Index + # @return [Cassandra::Table] table that the index applies to. + attr_reader :table + # @return [String] name of the index. + attr_reader :name + # @return [Symbol] kind of index: `:keys`, `:composites`, or `:custom`. + attr_reader :kind + # @return [String] name of column that the index applies to. + attr_reader :target + # @return [Hash] options of the index. + attr_reader :options + + # @private + def initialize(table, + name, + kind, + target, + options) + @table = table + @name = name.freeze + @kind = kind + @options = options.freeze + + # Target is a bit tricky; it may be an escaped name or not + # depending on C* version. Unify to be unescaped since a user + # who wants to know the target would want the bare column name. + + @target = if target[0] == '"' + target[1..-2] + else + target + end.freeze + end + + # @return [Boolean] whether or not this index uses a custom class. + def custom_index? + !@options['class_name'].nil? + end + + # @return [String] name of the index class if this is a custom index; nil otherwise. + def custom_class_name + @options['class_name'] + end + + # @return [String] a cql representation of this index + def to_cql + keyspace_name = Util.escape_name(@table.keyspace.name) + table_name = Util.escape_name(@table.name) + index_name = Util.escape_name(@name) + + # Target is interesting in that it's not necessarily a column name, + # so we can't simply escape it. If it contains a paren, we take it as is, + # otherwise assume it's a column name and escape accordingly. + escaped_target = @target.include?('(') ? @target : Util.escape_name(@target) + + if custom_index? + "CREATE CUSTOM INDEX #{index_name} ON #{keyspace_name}.#{table_name} (#{escaped_target}) " \ + "USING '#{@options['class_name']}'#{options_cql};" + else + "CREATE INDEX #{index_name} ON #{keyspace_name}.#{table_name} (#{escaped_target});" + end + end + + # @private + def eql?(other) + other.is_a?(Index) && + @table == other.table && + @name == other.name && + @kind == other.kind && + @target == other.target && + @options == other.options + end + alias == eql? + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "@name=#{@name.inspect} @table=#{@table.inspect} @kind=#{@kind.inspect} @target=#{@target.inspect}>" + end + + private + + def options_cql + # exclude 'class_name', 'target' keys + filtered_options = @options.reject do |key, _| + key == 'class_name' || key == 'target' + end + return '' if filtered_options.empty? + + result = ' WITH OPTIONS = {' + result << filtered_options.map do |key, value| + "'#{key}': '#{value}'" + end.join(', ') + result << '}' + result + end + end +end diff --git a/lib/cassandra/keyspace.rb b/lib/cassandra/keyspace.rb index 215c787e6..3a67b5959 100644 --- a/lib/cassandra/keyspace.rb +++ b/lib/cassandra/keyspace.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,7 +57,8 @@ def initialize(name, tables, types, functions, - aggregates) + aggregates, + views) @name = name @durable_writes = durable_writes @replication = replication @@ -65,6 +66,22 @@ def initialize(name, @types = types @functions = functions @aggregates = aggregates + @views = views + + # Set the keyspace attribute on the tables and views. + # Also set up the index collection on the keyspace and the view-collection on each table. + @indexes_hash = {} + @tables.each_value do |t| + t.set_keyspace(self) + t.each_index do |index| + @indexes_hash[index.name] = index + end + end + @views.each_value do |v| + v.set_keyspace(self) + table = v.base_table + table.add_view(v) if table + end end # @return [Boolean] whether durables writes are enabled for this keyspace @@ -100,6 +117,70 @@ def each_table(&block) end alias tables each_table + # @return [Boolean] whether this keyspace has an index with the given name + # @param name [String] index name + def has_index?(name) + @indexes_hash.key?(name) + end + + # @return [Cassandra::Index, nil] an index or nil + # @param name [String] index name + def index(name) + @indexes_hash[name] + end + + # Yield or enumerate each index defined in this keyspace + # @overload each_index + # @yieldparam index [Cassandra::Index] current index + # @return [Cassandra::Keyspace] self + # @overload each_index + # @return [Array] a list of indexes + def each_index(&block) + if block_given? + @indexes_hash.each_value(&block) + self + else + @indexes_hash.values + end + end + alias indexes each_index + + # @return [Boolean] whether this keyspace has a materialized view with the given name + # @param name [String] materialized view name + def has_materialized_view?(name) + # We check if the view exists *and* that its base-table is set. If base-table isn't available, + # it will be soon, so the user can poll on this method until we return a fully-baked materialized view. + @views.key?(name) && !@views[name].base_table.nil? + end + + # @return [Cassandra::MaterializedView, nil] a materialized view or nil + # @param name [String] materialized view name + def materialized_view(name) + @views[name] if has_materialized_view?(name) + end + + # Yield or enumerate each materialized view defined in this keyspace + # @overload each_materialized_view + # @yieldparam view [Cassandra::MaterializedView] current materialized view + # @return [Cassandra::Keyspace] self + # @overload each_materialized_view + # @return [Array] a list of materialized views + def each_materialized_view(&block) + if block_given? + @views.each_value do |v| + yield(v) if v.base_table + end + self + else + result = [] + @views.each_value do |v| + result << v if v.base_table + end + result + end + end + alias materialized_views each_materialized_view + # @return [Boolean] whether this keyspace has a user-defined type with the # given name # @param name [String] user-defined type name @@ -228,7 +309,8 @@ def update_table(table) tables, @types, @functions, - @aggregates) + @aggregates, + @views) end # @private @@ -241,7 +323,36 @@ def delete_table(table_name) tables, @types, @functions, - @aggregates) + @aggregates, + @views) + end + + # @private + def update_materialized_view(view) + views = @views.dup + views[view.name] = view + Keyspace.new(@name, + @durable_writes, + @replication, + @tables, + @types, + @functions, + @aggregates, + views) + end + + # @private + def delete_materialized_view(view_name) + views = @views.dup + views.delete(view_name) + Keyspace.new(@name, + @durable_writes, + @replication, + @tables, + @types, + @functions, + @aggregates, + views) end # @private @@ -254,7 +365,8 @@ def update_type(type) @tables, types, @functions, - @aggregates) + @aggregates, + @views) end # @private @@ -267,7 +379,8 @@ def delete_type(type_name) @tables, types, @functions, - @aggregates) + @aggregates, + @views) end # @private @@ -280,7 +393,8 @@ def update_function(function) @tables, @types, functions, - @aggregates) + @aggregates, + @views) end # @private @@ -293,7 +407,8 @@ def delete_function(function_name, function_args) @tables, @types, functions, - @aggregates) + @aggregates, + @views) end # @private @@ -306,7 +421,8 @@ def update_aggregate(aggregate) @tables, @types, @functions, - aggregates) + aggregates, + @views) end # @private @@ -319,7 +435,8 @@ def delete_aggregate(aggregate_name, aggregate_args) @tables, @types, @functions, - aggregates) + aggregates, + @views) end # @private @@ -333,6 +450,11 @@ def raw_tables @tables end + # @private + def raw_materialized_views + @views + end + # @private def raw_types @types diff --git a/lib/cassandra/listener.rb b/lib/cassandra/listener.rb index 6ca0c0e52..c4d7ad771 100644 --- a/lib/cassandra/listener.rb +++ b/lib/cassandra/listener.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/load_balancing.rb b/lib/cassandra/load_balancing.rb index 6f4c68cca..bc526e949 100644 --- a/lib/cassandra/load_balancing.rb +++ b/lib/cassandra/load_balancing.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/load_balancing/policies.rb b/lib/cassandra/load_balancing/policies.rb index 828c0b891..16a1aacd2 100644 --- a/lib/cassandra/load_balancing/policies.rb +++ b/lib/cassandra/load_balancing/policies.rb @@ -1,5 +1,5 @@ #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb b/lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb index f319e65a8..487c961d0 100644 --- a/lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb +++ b/lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,11 @@ module Cassandra module LoadBalancing module Policies class DCAwareRoundRobin < Policy + # @private + LOCAL_CONSISTENCIES = [:local_quorum, :local_one].freeze + # @private + EMPTY_ARRAY = [].freeze + # @private class Plan def initialize(local, remote, index) @@ -57,14 +62,12 @@ def next include MonitorMixin def initialize(datacenter = nil, - max_remote_hosts_to_use = nil, + max_remote_hosts_to_use = 0, use_remote_hosts_for_local_consistency = false) datacenter &&= String(datacenter) max_remote_hosts_to_use &&= Integer(max_remote_hosts_to_use) - unless datacenter.nil? - Util.assert_not_empty(datacenter) { 'datacenter cannot be empty' } - end + Util.assert_not_empty(datacenter) { 'datacenter cannot be empty' } unless datacenter.nil? unless max_remote_hosts_to_use.nil? Util.assert(max_remote_hosts_to_use >= 0) do @@ -72,6 +75,13 @@ def initialize(datacenter = nil, end end + # If use_remote* is true, max_remote* must be > 0 + if use_remote_hosts_for_local_consistency + Util.assert(max_remote_hosts_to_use.nil? || max_remote_hosts_to_use > 0, + 'max_remote_hosts_to_use must be nil (meaning unlimited) or > 0 when ' \ + 'use_remote_hosts_for_local_consistency is true') + end + @datacenter = datacenter @max_remote = max_remote_hosts_to_use @local = ::Array.new @@ -146,12 +156,16 @@ def plan(keyspace, statement, options) Plan.new(local, remote, position) end - private - - # @private - LOCAL_CONSISTENCIES = [:local_quorum, :local_one].freeze # @private - EMPTY_ARRAY = [].freeze + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "datacenter=#{@datacenter.inspect}, " \ + "use_remote=#{@use_remote.inspect}, " \ + "max_remote=#{@max_remote.inspect}, " \ + "local=#{@local.inspect}, " \ + "remote=#{@remote.inspect}, " \ + "position=#{@position.inspect}>" + end end end end diff --git a/lib/cassandra/load_balancing/policies/round_robin.rb b/lib/cassandra/load_balancing/policies/round_robin.rb index 8f2c83c04..040f51847 100644 --- a/lib/cassandra/load_balancing/policies/round_robin.rb +++ b/lib/cassandra/load_balancing/policies/round_robin.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -128,6 +128,13 @@ def plan(keyspace, statement, options) Plan.new(hosts, position) end + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "hosts=#{@hosts.inspect}, " \ + "position=#{@position.inspect}>" + end end end end diff --git a/lib/cassandra/load_balancing/policies/token_aware.rb b/lib/cassandra/load_balancing/policies/token_aware.rb index 1bf1ebb57..3d9328da5 100644 --- a/lib/cassandra/load_balancing/policies/token_aware.rb +++ b/lib/cassandra/load_balancing/policies/token_aware.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -136,6 +136,13 @@ def plan(keyspace, statement, options) Plan.new(replicas, @policy, keyspace, statement, options) end + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "policy=#{@policy.inspect}, " \ + "shuffle=#{@shuffle.inspect}>" + end end end end diff --git a/lib/cassandra/load_balancing/policies/white_list.rb b/lib/cassandra/load_balancing/policies/white_list.rb index fffd5b530..4e2f0dacd 100644 --- a/lib/cassandra/load_balancing/policies/white_list.rb +++ b/lib/cassandra/load_balancing/policies/white_list.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -87,6 +87,13 @@ def host_up(host) def host_down(host) @policy.host_down(host) if @ips.include?(host.ip) end + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "policy=#{@policy.inspect}, " \ + "ips=#{@ips.inspect}>" + end end end end diff --git a/lib/cassandra/materialized_view.rb b/lib/cassandra/materialized_view.rb new file mode 100644 index 000000000..3200baaa7 --- /dev/null +++ b/lib/cassandra/materialized_view.rb @@ -0,0 +1,92 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + # Represents a cassandra materialized view + # @see Cassandra::Keyspace#each_materialized_view + # @see Cassandra::Keyspace#materialized_view + class MaterializedView < ColumnContainer + # @private + def initialize(keyspace, + name, + partition_key, + clustering_columns, + other_columns, + options, + include_all_columns, + where_clause, + base_table_name, + id) + super(keyspace, name, partition_key, clustering_columns, other_columns, options, id) + @include_all_columns = include_all_columns + @where_clause = where_clause + @base_table_name = base_table_name + end + + # @return [Table] the table that this materialized view applies to. + def base_table + @keyspace.table(@base_table_name) + end + + # @return [String] a cql representation of this materialized view + def to_cql + keyspace_name = Util.escape_name(@keyspace.name) + cql = "CREATE MATERIALIZED VIEW #{keyspace_name}.#{Util.escape_name(@name)} AS\nSELECT " + cql << if @include_all_columns + '*' + else + @columns.map do |column| + Util.escape_name(column.name) + end.join(', ') + end + cql << "\nFROM #{keyspace_name}.#{Util.escape_name(@base_table_name)}" + cql << "\nWHERE #{@where_clause}" if @where_clause + cql << "\nPRIMARY KEY ((" + cql << @partition_key.map do |column| + Util.escape_name(column.name) + end.join(', ') + cql << ')' + unless @clustering_columns.empty? + cql << ', ' + cql << @clustering_columns.map do |column| + Util.escape_name(column.name) + end.join(', ') + end + cql << ")\nWITH #{@options.to_cql.split("\n").join("\n ")};" + end + + # @private + def eql?(other) + other.is_a?(MaterializedView) && + super.eql?(other) && + @include_all_columns == other.include_all_columns && + @where_clause == other.where_clause && + @base_table_name == other.base_table.name + end + alias == eql? + + private + + # We need these accessors for eql? to work, but we don't want random users to + # get these. + + # @private + attr_reader :include_all_columns, :where_clause + protected :include_all_columns, :where_clause + end +end diff --git a/lib/cassandra/null_logger.rb b/lib/cassandra/null_logger.rb index f6dfe6308..d19a79520 100644 --- a/lib/cassandra/null_logger.rb +++ b/lib/cassandra/null_logger.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol.rb b/lib/cassandra/protocol.rb index 819d7b3a2..a3136734e 100644 --- a/lib/cassandra/protocol.rb +++ b/lib/cassandra/protocol.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,6 +28,13 @@ module Formats BYTES_FORMAT = 'C*'.freeze TWO_INTS_FORMAT = 'NN'.freeze + + # All of the formats above are big-endian (e.g. network-byte-order). Some payloads (custom types) may have + # little-endian components. + + DOUBLE_FORMAT_LE = 'E'.freeze + INT_FORMAT_LE = 'V'.freeze + SHORT_FORMAT_LE = 'v'.freeze end module Constants @@ -42,6 +49,11 @@ module Constants SCHEMA_CHANGE_TARGET_FUNCTION = 'FUNCTION'.freeze SCHEMA_CHANGE_TARGET_AGGREGATE = 'AGGREGATE'.freeze end + + module Versions + BETA_VERSION = 5 + MAX_SUPPORTED_VERSION = 4 + end end end @@ -88,3 +100,7 @@ module Constants require 'cassandra/protocol/v3' require 'cassandra/protocol/v4' require 'cassandra/protocol/coder' +require 'cassandra/protocol/builders' +require 'cassandra/protocol/builders/registry' +require 'cassandra/protocol/builders/hash_builder' + diff --git a/lib/cassandra/protocol/builders.rb b/lib/cassandra/protocol/builders.rb new file mode 100644 index 000000000..b6e465c1d --- /dev/null +++ b/lib/cassandra/protocol/builders.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module Protocol + module Builders + module_function + + def row_registry + @row_registry ||= Registry.new(HashBuilder) + end + + # def udt_registry + # @row_registry ||= Registry.new(UDTBuilder) + # end + + end + end +end \ No newline at end of file diff --git a/lib/cassandra/protocol/builders/hash_builder.rb b/lib/cassandra/protocol/builders/hash_builder.rb new file mode 100644 index 000000000..2e7943c9c --- /dev/null +++ b/lib/cassandra/protocol/builders/hash_builder.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module Protocol + module Builders + class HashBuilder + + def initialize + @hash = Hash.new + end + + def []=(k, v) + @hash[k] = v + end + + def build + @hash + end + end + end + end +end diff --git a/lib/cassandra/protocol/builders/registry.rb b/lib/cassandra/protocol/builders/registry.rb new file mode 100644 index 000000000..4eaa32e72 --- /dev/null +++ b/lib/cassandra/protocol/builders/registry.rb @@ -0,0 +1,94 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module Protocol + module Builders + # @private + class Registry + include MonitorMixin + + def initialize(default_builder) + @builders = ::Hash.new + + mon_initialize + self.default = default_builder + end + + def fetch(keyspace, identifier) + @builders[determine_key(keyspace, identifier)] + end + + def add(keyspace, identifier, builder) + validate_builder(builder) + key = determine_key(keyspace, identifier) + + synchronize do + builders = @builders.dup + builders[key] = builder + @builders = builders + end + + self + end + + def remove(keyspace, identifier) + key = determine_key(keyspace, identifier) + + synchronize do + builders = @builders.dup + builders.delete(key) + @builders = builders + end + + self + end + + def default=(builder) + validate_builder(builder) + + synchronize do + @builders.default = builder + end + + builder + end + + private + + def determine_key(keyspace, identifier) + "#{keyspace}.#{identifier}" + end + + def validate_builder(builder) + unless valid_builder?(builder) + raise ::ArgumentError, + "#{builder} is not a valid builder class. " \ + 'Builders must respond to .new, #[]=, and #build' + end + end + + def valid_builder?(builder) + builder.respond_to?(:new) && + builder.method_defined?(:[]=) && + builder.method_defined?(:build) + end + end + end + end +end diff --git a/lib/cassandra/protocol/coder.rb b/lib/cassandra/protocol/coder.rb index 5ff7fc3ab..faf3f4648 100644 --- a/lib/cassandra/protocol/coder.rb +++ b/lib/cassandra/protocol/coder.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ module Coder NO_METADATA_FLAG = 0x04 def write_values_v4(buffer, values, types, names = EMPTY_LIST) - if values && values.size > 0 + if values && !values.empty? buffer.append_short(values.size) values.zip(types, names) do |(value, type, name)| buffer.append_string(name) if name @@ -97,6 +97,7 @@ def write_value_v4(buffer, value, type) when :bigint, :counter then write_bigint(buffer, value) when :blob then write_blob(buffer, value) when :boolean then write_boolean(buffer, value) + when :custom then write_custom(buffer, value, type) when :decimal then write_decimal(buffer, value) when :double then write_double(buffer, value) when :float then write_float(buffer, value) @@ -221,19 +222,21 @@ def read_type_v4(buffer) end end - def read_values_v4(buffer, column_metadata) + def read_values_v4(buffer, column_metadata, custom_type_handlers) ::Array.new(buffer.read_int) do |_i| - row = ::Hash.new + metadata = column_metadata.first + builder = Builders.row_registry.fetch(metadata[0], metadata[1]) + row = builder.new column_metadata.each do |(_, _, column, type)| - row[column] = read_value_v4(buffer, type) + row[column] = read_value_v4(buffer, type, custom_type_handlers) end - row + row.build end end - def read_value_v4(buffer, type) + def read_value_v4(buffer, type, custom_type_handlers) case type.kind when :ascii then read_ascii(buffer) when :bigint, :counter then read_bigint(buffer) @@ -253,11 +256,12 @@ def read_value_v4(buffer, type) when :smallint then read_smallint(buffer) when :time then read_time(buffer) when :date then read_date(buffer) + when :custom then read_custom(buffer, type, custom_type_handlers) when :list return nil unless read_size(buffer) value_type = type.value_type - ::Array.new(buffer.read_signed_int) { read_value_v4(buffer, value_type) } + ::Array.new(buffer.read_signed_int) { read_value_v4(buffer, value_type, custom_type_handlers) } when :map return nil unless read_size(buffer) @@ -266,7 +270,8 @@ def read_value_v4(buffer, type) value = ::Hash.new buffer.read_signed_int.times do - value[read_value_v4(buffer, key_type)] = read_value_v4(buffer, value_type) + value[read_value_v4(buffer, key_type, custom_type_handlers)] = + read_value_v4(buffer, value_type, custom_type_handlers) end value @@ -277,7 +282,7 @@ def read_value_v4(buffer, type) value = ::Set.new buffer.read_signed_int.times do - value << read_value_v4(buffer, value_type) + value << read_value_v4(buffer, value_type, custom_type_handlers) end value @@ -295,7 +300,7 @@ def read_value_v4(buffer, type) values[field.name] = if length - buffer.length >= size nil else - read_value_v4(buffer, field.type) + read_value_v4(buffer, field.type, custom_type_handlers) end end @@ -308,7 +313,7 @@ def read_value_v4(buffer, type) members.each do |member_type| break if buffer.empty? - values << read_value_v4(buffer, member_type) + values << read_value_v4(buffer, member_type, custom_type_handlers) end values.fill(nil, values.length, (members.length - values.length)) @@ -320,7 +325,7 @@ def read_value_v4(buffer, type) end def write_values_v3(buffer, values, types, names = EMPTY_LIST) - if values && values.size > 0 + if values && !values.empty? buffer.append_short(values.size) values.zip(types, names) do |(value, type, name)| buffer.append_string(name) if name @@ -571,7 +576,7 @@ def read_type_v3(buffer) end def write_values_v1(buffer, values, types) - if values && values.size > 0 + if values && !values.empty? buffer.append_short(values.size) values.each_with_index do |value, index| write_value_v1(buffer, value, types[index]) @@ -774,6 +779,15 @@ def read_boolean(buffer) read_size(buffer) && buffer.read(1) == Constants::TRUE_BYTE end + def read_custom(buffer, type, custom_type_handlers) + # Lookup the type-name to get the Class that can deserialize buffer data into a custom domain object. + unless custom_type_handlers && custom_type_handlers.key?(type) + raise Errors::DecodingError, %(Unsupported custom column type: #{type.name}) + end + num_bytes = read_size(buffer) + custom_type_handlers[type].deserialize(buffer.read(num_bytes)) if num_bytes && num_bytes > 0 + end + def read_decimal(buffer) size = read_size(buffer) size && buffer.read_decimal(size) @@ -860,6 +874,15 @@ def write_boolean(buffer, value) buffer.append(value ? Constants::TRUE_BYTE : Constants::FALSE_BYTE) end + def write_custom(buffer, value, type) + # Verify that the given type-name matches the value's cql type name. + if value.class.type != type + raise Errors::EncodingError, "type mismatch: value is a #{value.type} and column is a #{type}" + end + + buffer.append_bytes(value.serialize) + end + def write_decimal(buffer, value) buffer.append_bytes(CqlByteBuffer.new.append_decimal(value)) end diff --git a/lib/cassandra/protocol/cql_byte_buffer.rb b/lib/cassandra/protocol/cql_byte_buffer.rb index 955714080..ada91e546 100644 --- a/lib/cassandra/protocol/cql_byte_buffer.rb +++ b/lib/cassandra/protocol/cql_byte_buffer.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,17 @@ module Cassandra module Protocol class CqlByteBuffer < Ione::ByteBuffer + # @private + MINUS = '-'.freeze + # @private + ZERO = '0'.freeze + # @private + DECIMAL_POINT = '.'.freeze + # @private + FLOAT_STRING_FORMAT = 'F'.freeze + # @private + NO_CHAR = ''.freeze + def inspect "#<#{self.class.name}:0x#{object_id.to_s(16)} #{to_str.inspect}>" end @@ -81,6 +92,13 @@ def read_double "Not enough bytes available to decode a double: #{e.message}", e.backtrace end + def read_double_le + read(8).unpack(Formats::DOUBLE_FORMAT_LE).first + rescue RangeError => e + raise Errors::DecodingError, + "Not enough bytes available to decode a double: #{e.message}", e.backtrace + end + def read_float read(4).unpack(Formats::FLOAT_FORMAT).first rescue RangeError => e @@ -97,6 +115,13 @@ def read_signed_int "Not enough bytes available to decode an int: #{e.message}", e.backtrace end + def read_unsigned_int_le + read(4).unpack(Formats::INT_FORMAT_LE).first + rescue RangeError => e + raise Errors::DecodingError, + "Not enough bytes available to decode an int: #{e.message}", e.backtrace + end + def read_unsigned_short read_short rescue RangeError => e @@ -104,6 +129,13 @@ def read_unsigned_short "Not enough bytes available to decode a short: #{e.message}", e.backtrace end + def read_unsigned_short_le + read(2).unpack(Formats::SHORT_FORMAT_LE).first + rescue RangeError => e + raise Errors::DecodingError, + "Not enough bytes available to decode a short: #{e.message}", e.backtrace + end + def read_string length = read_unsigned_short string = read(length) @@ -172,6 +204,15 @@ def read_inet e.backtrace end + def read_inet_addr + size = read_byte + IPAddr.new_ntoh(read(size)) + rescue RangeError => e + raise Errors::DecodingError, + "Not enough bytes available to decode an INET addr: #{e.message}", + e.backtrace + end + def read_consistency index = read_unsigned_short if index >= CONSISTENCIES.size || CONSISTENCIES[index].nil? @@ -200,6 +241,18 @@ def read_bytes_map map end + def read_reason_map + # reason_map is new in v5. Starts with an int indicating the number of key-value pairs, followed by + # the key-value pairs. Keys are inet's, values are short int's. + map = {} + map_size = read_int + map_size.times do + key = read_inet_addr + map[key] = read_short + end + map + end + def read_string_multimap map = {} map_size = read_unsigned_short @@ -291,9 +344,7 @@ def append_short_bytes(bytes) def append_consistency(consistency) index = CONSISTENCIES.index(consistency) - if index.nil? || CONSISTENCIES[index].nil? - raise Errors::EncodingError, %(Unknown consistency "#{consistency}") - end + raise Errors::EncodingError, %(Unknown consistency "#{consistency}") if index.nil? || CONSISTENCIES[index].nil? append_short(index) end @@ -315,10 +366,6 @@ def append_bytes_map(map) self end - def append_timestamp(timestamp) - append_long(timestamp.tv_sec * 1000000 + timestamp.tv_usec) - end - def append_long(n) top = n >> 32 bottom = n & 0xffffffff @@ -360,14 +407,6 @@ def eql?(other) other.eql?(to_str) end alias == eql? - - private - - MINUS = '-'.freeze - ZERO = '0'.freeze - DECIMAL_POINT = '.'.freeze - FLOAT_STRING_FORMAT = 'F'.freeze - NO_CHAR = ''.freeze end end end diff --git a/lib/cassandra/protocol/cql_protocol_handler.rb b/lib/cassandra/protocol/cql_protocol_handler.rb index e359e06b9..e9d20d6c9 100644 --- a/lib/cassandra/protocol/cql_protocol_handler.rb +++ b/lib/cassandra/protocol/cql_protocol_handler.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -46,7 +46,8 @@ def initialize(connection, compressor = nil, heartbeat_interval = 30, idle_timeout = 60, - requests_per_connection = 128) + requests_per_connection = 128, + custom_type_handlers = {}) @protocol_version = protocol_version @connection = connection @scheduler = scheduler @@ -60,7 +61,7 @@ def initialize(connection, if protocol_version > 3 @frame_encoder = V4::Encoder.new(@compressor, protocol_version) - @frame_decoder = V4::Decoder.new(self, @compressor) + @frame_decoder = V4::Decoder.new(self, @compressor, custom_type_handlers) elsif protocol_version > 2 @frame_encoder = V3::Encoder.new(@compressor, protocol_version) @frame_decoder = V3::Decoder.new(self, @compressor) @@ -236,9 +237,7 @@ def complete_request(id, response) ensure @lock.unlock end - if response.is_a?(Protocol::SetKeyspaceResultResponse) - @keyspace = response.keyspace - end + @keyspace = response.keyspace if response.is_a?(Protocol::SetKeyspaceResultResponse) if response.is_a?(Protocol::SchemaChangeResultResponse) && response.change == 'DROPPED' && response.keyspace == @keyspace && @@ -253,7 +252,10 @@ def complete_request(id, response) # @private class RequestPromise < Ione::Promise + extend AttrBoolean + attr_reader :request, :timeout + attr_boolean :timed_out def initialize(request, timeout) @request = request @@ -262,10 +264,6 @@ def initialize(request, timeout) super() end - def timed_out? - @timed_out - end - def time_out! unless future.completed? @timed_out = true diff --git a/lib/cassandra/protocol/request.rb b/lib/cassandra/protocol/request.rb index 503c0efb2..6c1b0035d 100644 --- a/lib/cassandra/protocol/request.rb +++ b/lib/cassandra/protocol/request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,17 +19,16 @@ module Cassandra module Protocol class Request - attr_reader :opcode, :trace + extend AttrBoolean + + attr_reader :opcode + attr_boolean :trace def initialize(opcode, trace = false) @opcode = opcode @trace = trace end - def trace? - @trace - end - def compressable? true end diff --git a/lib/cassandra/protocol/requests/auth_response_request.rb b/lib/cassandra/protocol/requests/auth_response_request.rb index 6708e3402..4115b28c0 100644 --- a/lib/cassandra/protocol/requests/auth_response_request.rb +++ b/lib/cassandra/protocol/requests/auth_response_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/requests/batch_request.rb b/lib/cassandra/protocol/requests/batch_request.rb index 1cbc55646..5d37f516e 100644 --- a/lib/cassandra/protocol/requests/batch_request.rb +++ b/lib/cassandra/protocol/requests/batch_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ def write(buffer, protocol_version, encoder) buffer.append(flags.chr) buffer.append_consistency(@serial_consistency) if @serial_consistency - buffer.append_timestamp(@timestamp) if @timestamp + buffer.append_long(@timestamp) if @timestamp end buffer diff --git a/lib/cassandra/protocol/requests/credentials_request.rb b/lib/cassandra/protocol/requests/credentials_request.rb index 06bc5b8f3..e476db57a 100644 --- a/lib/cassandra/protocol/requests/credentials_request.rb +++ b/lib/cassandra/protocol/requests/credentials_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/requests/execute_request.rb b/lib/cassandra/protocol/requests/execute_request.rb index 39dd117f7..692c051c9 100644 --- a/lib/cassandra/protocol/requests/execute_request.rb +++ b/lib/cassandra/protocol/requests/execute_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,9 +44,7 @@ def initialize(id, unless serial_consistency.nil? || CONSISTENCIES.include?(serial_consistency) raise ArgumentError, %(No such consistency: #{serial_consistency.inspect}) end - if paging_state && !page_size - raise ArgumentError, %(Paging state given but no page size) - end + raise ArgumentError, %(Paging state given but no page size) if paging_state && !page_size super(10, trace) @id = id @metadata = metadata @@ -69,18 +67,18 @@ def write(buffer, protocol_version, encoder) if protocol_version > 1 buffer.append_consistency(@consistency) flags = 0 - flags |= 0x01 if @values.size > 0 + flags |= 0x01 unless @values.empty? flags |= 0x02 unless @request_metadata flags |= 0x04 if @page_size flags |= 0x08 if @paging_state flags |= 0x10 if @serial_consistency flags |= 0x20 if protocol_version > 2 && @timestamp buffer.append(flags.chr) - encoder.write_parameters(buffer, @values, @metadata) if @values.size > 0 + encoder.write_parameters(buffer, @values, @metadata) unless @values.empty? buffer.append_int(@page_size) if @page_size buffer.append_bytes(@paging_state) if @paging_state buffer.append_consistency(@serial_consistency) if @serial_consistency - buffer.append_timestamp(@timestamp) if protocol_version > 2 && @timestamp + buffer.append_long(@timestamp) if protocol_version > 2 && @timestamp else encoder.write_parameters(buffer, @values, @metadata) buffer.append_consistency(@consistency) diff --git a/lib/cassandra/protocol/requests/options_request.rb b/lib/cassandra/protocol/requests/options_request.rb index 8a5f566f9..23f73f845 100644 --- a/lib/cassandra/protocol/requests/options_request.rb +++ b/lib/cassandra/protocol/requests/options_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/requests/prepare_request.rb b/lib/cassandra/protocol/requests/prepare_request.rb index 9f2974688..b2c6850a0 100644 --- a/lib/cassandra/protocol/requests/prepare_request.rb +++ b/lib/cassandra/protocol/requests/prepare_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/requests/query_request.rb b/lib/cassandra/protocol/requests/query_request.rb index f634aadd7..42e9c67ed 100644 --- a/lib/cassandra/protocol/requests/query_request.rb +++ b/lib/cassandra/protocol/requests/query_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ def write(buffer, protocol_version, encoder) flags |= 0x08 if @paging_state flags |= 0x10 if @serial_consistency flags |= 0x20 if protocol_version > 2 && @timestamp - if @values && @values.size > 0 + if @values && !@values.empty? flags |= 0x01 flags |= 0x40 if protocol_version > 2 && !@names.empty? buffer.append(flags.chr) @@ -71,7 +71,7 @@ def write(buffer, protocol_version, encoder) buffer.append_int(@page_size) if @page_size buffer.append_bytes(@paging_state) if @paging_state buffer.append_consistency(@serial_consistency) if @serial_consistency - buffer.append_timestamp(@timestamp) if protocol_version > 2 && @timestamp + buffer.append_long(@timestamp) if protocol_version > 2 && @timestamp end buffer end diff --git a/lib/cassandra/protocol/requests/register_request.rb b/lib/cassandra/protocol/requests/register_request.rb index 03259db5a..76c90839e 100644 --- a/lib/cassandra/protocol/requests/register_request.rb +++ b/lib/cassandra/protocol/requests/register_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/requests/startup_request.rb b/lib/cassandra/protocol/requests/startup_request.rb index 48a43fbf4..cf9be7ce2 100644 --- a/lib/cassandra/protocol/requests/startup_request.rb +++ b/lib/cassandra/protocol/requests/startup_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,13 +19,16 @@ module Cassandra module Protocol class StartupRequest < Request + # @private + CQL_VERSION = 'CQL_VERSION'.freeze + # @private + COMPRESSION = 'COMPRESSION'.freeze + attr_reader :options def initialize(cql_version, compression = nil) super(1) - unless cql_version - raise ArgumentError, "Invalid CQL version: #{cql_version.inspect}" - end + raise ArgumentError, "Invalid CQL version: #{cql_version.inspect}" unless cql_version @options = {CQL_VERSION => cql_version} @options[COMPRESSION] = compression if compression end @@ -41,11 +44,6 @@ def write(buffer, protocol_version, encoder) def to_s %(STARTUP #{@options}) end - - private - - CQL_VERSION = 'CQL_VERSION'.freeze - COMPRESSION = 'COMPRESSION'.freeze end end end diff --git a/lib/cassandra/protocol/requests/void_query_request.rb b/lib/cassandra/protocol/requests/void_query_request.rb index 749b167a3..80a972cbd 100644 --- a/lib/cassandra/protocol/requests/void_query_request.rb +++ b/lib/cassandra/protocol/requests/void_query_request.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/response.rb b/lib/cassandra/protocol/response.rb index b58fd15ee..e46187972 100644 --- a/lib/cassandra/protocol/response.rb +++ b/lib/cassandra/protocol/response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,8 +19,7 @@ module Cassandra module Protocol class Response - private - + # @private RESPONSE_TYPES = [ # populated by subclasses ] diff --git a/lib/cassandra/protocol/responses/already_exists_error_response.rb b/lib/cassandra/protocol/responses/already_exists_error_response.rb index 7a67a4d7b..670a1151b 100644 --- a/lib/cassandra/protocol/responses/already_exists_error_response.rb +++ b/lib/cassandra/protocol/responses/already_exists_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/responses/auth_challenge_response.rb b/lib/cassandra/protocol/responses/auth_challenge_response.rb index b69f7f1ad..f47bd1e05 100644 --- a/lib/cassandra/protocol/responses/auth_challenge_response.rb +++ b/lib/cassandra/protocol/responses/auth_challenge_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class AuthChallengeResponse < Response + # @private + RESPONSE_TYPES[0x0e] = self + attr_reader :token def initialize(token) @@ -28,10 +31,6 @@ def initialize(token) def to_s %(AUTH_CHALLENGE #{@token.bytesize}) end - - private - - RESPONSE_TYPES[0x0e] = self end end end diff --git a/lib/cassandra/protocol/responses/auth_success_response.rb b/lib/cassandra/protocol/responses/auth_success_response.rb index d9d3cbfe4..91552870c 100644 --- a/lib/cassandra/protocol/responses/auth_success_response.rb +++ b/lib/cassandra/protocol/responses/auth_success_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class AuthSuccessResponse < Response + # @private + RESPONSE_TYPES[0x10] = self + attr_reader :token def initialize(token) @@ -28,10 +31,6 @@ def initialize(token) def to_s %(AUTH_SUCCESS #{@token && @token.bytesize}) end - - private - - RESPONSE_TYPES[0x10] = self end end end diff --git a/lib/cassandra/protocol/responses/authenticate_response.rb b/lib/cassandra/protocol/responses/authenticate_response.rb index f2bc650bf..7d894ee2a 100644 --- a/lib/cassandra/protocol/responses/authenticate_response.rb +++ b/lib/cassandra/protocol/responses/authenticate_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class AuthenticateResponse < Response + # @private + RESPONSE_TYPES[0x03] = self + attr_reader :authentication_class def initialize(authentication_class) @@ -28,10 +31,6 @@ def initialize(authentication_class) def to_s %(AUTHENTICATE #{authentication_class}) end - - private - - RESPONSE_TYPES[0x03] = self end end end diff --git a/lib/cassandra/protocol/responses/error_response.rb b/lib/cassandra/protocol/responses/error_response.rb index b0ad9f570..8ea4304e6 100644 --- a/lib/cassandra/protocol/responses/error_response.rb +++ b/lib/cassandra/protocol/responses/error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class ErrorResponse < Response + # @private + RESPONSE_TYPES[0x00] = self + attr_reader :code, :message, :custom_payload, :warnings def initialize(*args) @@ -134,10 +137,6 @@ def to_error(keyspace, statement, options, hosts, consistency, retries) retries) end end - - private - - RESPONSE_TYPES[0x00] = self end end end diff --git a/lib/cassandra/protocol/responses/event_response.rb b/lib/cassandra/protocol/responses/event_response.rb index 95f2fb9e6..2308594bc 100644 --- a/lib/cassandra/protocol/responses/event_response.rb +++ b/lib/cassandra/protocol/responses/event_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,10 +19,9 @@ module Cassandra module Protocol class EventResponse < ResultResponse - private - + # @private RESPONSE_TYPES[0x0c] = self - + # @private EVENT_TYPES = { # populated by subclasses } diff --git a/lib/cassandra/protocol/responses/function_failure_error_response.rb b/lib/cassandra/protocol/responses/function_failure_error_response.rb index b797254da..7d64fb416 100644 --- a/lib/cassandra/protocol/responses/function_failure_error_response.rb +++ b/lib/cassandra/protocol/responses/function_failure_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/responses/prepared_result_response.rb b/lib/cassandra/protocol/responses/prepared_result_response.rb index 83a5856b0..5c69afde5 100644 --- a/lib/cassandra/protocol/responses/prepared_result_response.rb +++ b/lib/cassandra/protocol/responses/prepared_result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class PreparedResultResponse < ResultResponse + # @private + RESULT_TYPES[0x04] = self + attr_reader :id, :metadata, :result_metadata, :pk_idx def initialize(custom_payload, @@ -54,10 +57,6 @@ def to_s hex_id = @id.each_byte.map { |x| x.to_s(16).rjust(2, '0') }.join('') %(RESULT PREPARED #{hex_id} #{@metadata}) end - - private - - RESULT_TYPES[0x04] = self end end end diff --git a/lib/cassandra/protocol/responses/raw_rows_result_response.rb b/lib/cassandra/protocol/responses/raw_rows_result_response.rb index 50268279b..9f7335985 100644 --- a/lib/cassandra/protocol/responses/raw_rows_result_response.rb +++ b/lib/cassandra/protocol/responses/raw_rows_result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,17 +24,19 @@ def initialize(custom_payload, protocol_version, raw_rows, paging_state, - trace_id) + trace_id, + custom_type_handlers = nil) super(custom_payload, warnings, nil, nil, paging_state, trace_id) @protocol_version = protocol_version @raw_rows = raw_rows + @custom_type_handlers = custom_type_handlers end def materialize(metadata) @metadata = metadata @rows = if @protocol_version == 4 - Coder.read_values_v4(@raw_rows, @metadata) + Coder.read_values_v4(@raw_rows, @metadata, @custom_type_handlers) elsif @protocol_version == 3 Coder.read_values_v3(@raw_rows, @metadata) else diff --git a/lib/cassandra/protocol/responses/read_failure_error_response.rb b/lib/cassandra/protocol/responses/read_failure_error_response.rb index 4be79984c..8eac8b09e 100644 --- a/lib/cassandra/protocol/responses/read_failure_error_response.rb +++ b/lib/cassandra/protocol/responses/read_failure_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ module Cassandra module Protocol class ReadFailureErrorResponse < ErrorResponse - attr_reader :consistency, :received, :blockfor, :numfailures, :data_present + attr_reader :consistency, :received, :blockfor, :numfailures, :data_present, :failures_by_node def initialize(custom_payload, warnings, @@ -29,14 +29,19 @@ def initialize(custom_payload, received, blockfor, numfailures, - data_present) + data_present, + failures_by_node) super(custom_payload, warnings, code, message) @consistency = consistency @received = received @blockfor = blockfor - @numfailures = numfailures @data_present = data_present + @failures_by_node = failures_by_node + + # If failures_by_node is set, numfailures isn't, and v.v. Set @numfailures to the size of the failure-map + # if numfailures is nil. + @numfailures = numfailures || @failures_by_node.size end def to_error(keyspace, statement, options, hosts, consistency, retries) @@ -53,7 +58,8 @@ def to_error(keyspace, statement, options, hosts, consistency, retries) @consistency, @blockfor, @numfailures, - @received) + @received, + @failures_by_node) end def to_s diff --git a/lib/cassandra/protocol/responses/read_timeout_error_response.rb b/lib/cassandra/protocol/responses/read_timeout_error_response.rb index 4b277b9a2..817c1aab9 100644 --- a/lib/cassandra/protocol/responses/read_timeout_error_response.rb +++ b/lib/cassandra/protocol/responses/read_timeout_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/responses/ready_response.rb b/lib/cassandra/protocol/responses/ready_response.rb index a44ad6be5..bdcc74cda 100644 --- a/lib/cassandra/protocol/responses/ready_response.rb +++ b/lib/cassandra/protocol/responses/ready_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class ReadyResponse < Response + # @private + RESPONSE_TYPES[0x02] = self + def eql?(rs) rs.is_a?(self.class) end @@ -35,10 +38,6 @@ def hash def to_s 'READY' end - - private - - RESPONSE_TYPES[0x02] = self end end end diff --git a/lib/cassandra/protocol/responses/result_response.rb b/lib/cassandra/protocol/responses/result_response.rb index 4db85461e..42798c6f3 100644 --- a/lib/cassandra/protocol/responses/result_response.rb +++ b/lib/cassandra/protocol/responses/result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,13 @@ module Cassandra module Protocol class ResultResponse < Response + # @private + RESPONSE_TYPES[0x08] = self + # @private + RESULT_TYPES = [ + # populated by subclasses + ] + attr_reader :custom_payload, :warnings, :trace_id def initialize(custom_payload, warnings, trace_id) @@ -30,14 +37,6 @@ def initialize(custom_payload, warnings, trace_id) def void? false end - - private - - RESPONSE_TYPES[0x08] = self - - RESULT_TYPES = [ - # populated by subclasses - ] end end end diff --git a/lib/cassandra/protocol/responses/rows_result_response.rb b/lib/cassandra/protocol/responses/rows_result_response.rb index ba1a90b5f..bf305f631 100644 --- a/lib/cassandra/protocol/responses/rows_result_response.rb +++ b/lib/cassandra/protocol/responses/rows_result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class RowsResultResponse < ResultResponse + # @private + RESULT_TYPES[0x02] = self + attr_reader :rows, :metadata, :paging_state def initialize(custom_payload, warnings, rows, metadata, paging_state, trace_id) @@ -31,10 +34,6 @@ def initialize(custom_payload, warnings, rows, metadata, paging_state, trace_id) def to_s %(RESULT ROWS #{@metadata} #{@rows}) end - - private - - RESULT_TYPES[0x02] = self end end end diff --git a/lib/cassandra/protocol/responses/schema_change_event_response.rb b/lib/cassandra/protocol/responses/schema_change_event_response.rb index faceedfc3..7927df852 100644 --- a/lib/cassandra/protocol/responses/schema_change_event_response.rb +++ b/lib/cassandra/protocol/responses/schema_change_event_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,6 +21,9 @@ module Protocol class SchemaChangeEventResponse < EventResponse TYPE = 'SCHEMA_CHANGE'.freeze + # @private + EVENT_TYPES[TYPE] = self + attr_reader :change, :keyspace, :name, :target, :arguments def initialize(change, keyspace, name, target, arguments) @@ -65,10 +68,6 @@ def to_s %(EVENT SCHEMA_CHANGE #{@change} #{@target} "#{@keyspace}" "#{@name}") end end - - private - - EVENT_TYPES[TYPE] = self end end end diff --git a/lib/cassandra/protocol/responses/schema_change_result_response.rb b/lib/cassandra/protocol/responses/schema_change_result_response.rb index 7463e05c5..7e7037f18 100644 --- a/lib/cassandra/protocol/responses/schema_change_result_response.rb +++ b/lib/cassandra/protocol/responses/schema_change_result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class SchemaChangeResultResponse < ResultResponse + # @private + RESULT_TYPES[0x05] = self + attr_reader :arguments, :change, :keyspace, :name, :target, :type def initialize(custom_payload, @@ -62,10 +65,6 @@ def hash def to_s %(RESULT SCHEMA_CHANGE #{@change} #{@target} "#{@keyspace}" "#{@name}") end - - private - - RESULT_TYPES[0x05] = self end end end diff --git a/lib/cassandra/protocol/responses/set_keyspace_result_response.rb b/lib/cassandra/protocol/responses/set_keyspace_result_response.rb index be41a96ca..509fb2749 100644 --- a/lib/cassandra/protocol/responses/set_keyspace_result_response.rb +++ b/lib/cassandra/protocol/responses/set_keyspace_result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class SetKeyspaceResultResponse < ResultResponse + # @private + RESULT_TYPES[0x03] = self + attr_reader :keyspace def initialize(custom_payload, warnings, keyspace, trace_id) @@ -29,10 +32,6 @@ def initialize(custom_payload, warnings, keyspace, trace_id) def to_s %(RESULT SET_KEYSPACE "#{@keyspace}") end - - private - - RESULT_TYPES[0x03] = self end end end diff --git a/lib/cassandra/protocol/responses/status_change_event_response.rb b/lib/cassandra/protocol/responses/status_change_event_response.rb index 8d79017ad..5a8e20986 100644 --- a/lib/cassandra/protocol/responses/status_change_event_response.rb +++ b/lib/cassandra/protocol/responses/status_change_event_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,6 +21,9 @@ module Protocol class StatusChangeEventResponse < EventResponse TYPE = 'STATUS_CHANGE'.freeze + # @private + EVENT_TYPES[TYPE] = self + attr_reader :type, :change, :address, :port def initialize(*args) @@ -31,10 +34,6 @@ def initialize(*args) def to_s %(EVENT #{@type} #{@change} #{@address}:#{@port}) end - - private - - EVENT_TYPES[TYPE] = self end end end diff --git a/lib/cassandra/protocol/responses/supported_response.rb b/lib/cassandra/protocol/responses/supported_response.rb index 8827d3960..daa3a6fe2 100644 --- a/lib/cassandra/protocol/responses/supported_response.rb +++ b/lib/cassandra/protocol/responses/supported_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class SupportedResponse < Response + # @private + RESPONSE_TYPES[0x06] = self + attr_reader :options def initialize(options) @@ -28,10 +31,6 @@ def initialize(options) def to_s %(SUPPORTED #{options}) end - - private - - RESPONSE_TYPES[0x06] = self end end end diff --git a/lib/cassandra/protocol/responses/topology_change_event_response.rb b/lib/cassandra/protocol/responses/topology_change_event_response.rb index 51b580aec..1f6493ece 100644 --- a/lib/cassandra/protocol/responses/topology_change_event_response.rb +++ b/lib/cassandra/protocol/responses/topology_change_event_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,14 +21,13 @@ module Protocol class TopologyChangeEventResponse < StatusChangeEventResponse TYPE = 'TOPOLOGY_CHANGE'.freeze + # @private + EVENT_TYPES[TYPE] = self + def initialize(*args) super @type = TYPE end - - private - - EVENT_TYPES[TYPE] = self end end end diff --git a/lib/cassandra/protocol/responses/unavailable_error_response.rb b/lib/cassandra/protocol/responses/unavailable_error_response.rb index bc04d9352..d6c83280d 100644 --- a/lib/cassandra/protocol/responses/unavailable_error_response.rb +++ b/lib/cassandra/protocol/responses/unavailable_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/responses/unprepared_error_response.rb b/lib/cassandra/protocol/responses/unprepared_error_response.rb index b2da1a9b1..4a73119cf 100644 --- a/lib/cassandra/protocol/responses/unprepared_error_response.rb +++ b/lib/cassandra/protocol/responses/unprepared_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/responses/void_result_response.rb b/lib/cassandra/protocol/responses/void_result_response.rb index 92656f36e..3cad53b5f 100644 --- a/lib/cassandra/protocol/responses/void_result_response.rb +++ b/lib/cassandra/protocol/responses/void_result_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ module Cassandra module Protocol class VoidResultResponse < ResultResponse + # @private + RESULT_TYPES[0x01] = self + def to_s %(RESULT VOID) end @@ -26,10 +29,6 @@ def to_s def void? true end - - private - - RESULT_TYPES[0x01] = self end end end diff --git a/lib/cassandra/protocol/responses/write_failure_error_response.rb b/lib/cassandra/protocol/responses/write_failure_error_response.rb index 3cecb1276..6a03eca3d 100644 --- a/lib/cassandra/protocol/responses/write_failure_error_response.rb +++ b/lib/cassandra/protocol/responses/write_failure_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ module Cassandra module Protocol class WriteFailureErrorResponse < ErrorResponse - attr_reader :consistency, :received, :blockfor, :numfailures, :write_type + attr_reader :consistency, :received, :blockfor, :numfailures, :write_type, :failures_by_node def initialize(custom_payload, warnings, @@ -29,16 +29,21 @@ def initialize(custom_payload, received, blockfor, numfailures, - write_type) + write_type, + failures_by_node) super(custom_payload, warnings, code, message) write_type.downcase! - @consistency = consistency - @received = received - @blockfor = blockfor - @numfailures = numfailures - @write_type = write_type.to_sym + @consistency = consistency + @received = received + @blockfor = blockfor + @write_type = write_type.to_sym + @failures_by_node = failures_by_node + + # If failures_by_node is set, numfailures isn't, and v.v. Set @numfailures to the size of the failure-map + # if numfailures is nil. + @numfailures = numfailures || @failures_by_node.size end def to_error(keyspace, statement, options, hosts, consistency, retries) @@ -55,7 +60,8 @@ def to_error(keyspace, statement, options, hosts, consistency, retries) @consistency, @blockfor, @numfailures, - @received) + @received, + @failures_by_node) end def to_s diff --git a/lib/cassandra/protocol/responses/write_timeout_error_response.rb b/lib/cassandra/protocol/responses/write_timeout_error_response.rb index 2354b5f43..a156a4929 100644 --- a/lib/cassandra/protocol/responses/write_timeout_error_response.rb +++ b/lib/cassandra/protocol/responses/write_timeout_error_response.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/protocol/v1.rb b/lib/cassandra/protocol/v1.rb index 36a9a24d6..d7217402c 100644 --- a/lib/cassandra/protocol/v1.rb +++ b/lib/cassandra/protocol/v1.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -155,8 +155,6 @@ def actual_decode(buffer, fields, size) end end - private - CODE_ERROR = 0x00 CODE_READY = 0x02 CODE_AUTHENTICATE = 0x03 @@ -243,7 +241,8 @@ def decode_response(opcode, protocol_version, buffer, size, trace_id) protocol_version, remaining_bytes, paging_state, - trace_id) + trace_id, + nil) else RowsResultResponse.new(nil, nil, @@ -258,9 +257,7 @@ def decode_response(opcode, protocol_version, buffer, size, trace_id) id = buffer.read_short_bytes params_metadata = Coder.read_metadata_v1(buffer).first result_metadata = nil - if protocol_version > 1 - result_metadata = Coder.read_metadata_v1(buffer).first - end + result_metadata = Coder.read_metadata_v1(buffer).first if protocol_version > 1 PreparedResultResponse.new(nil, nil, diff --git a/lib/cassandra/protocol/v3.rb b/lib/cassandra/protocol/v3.rb index be5630b8a..96b5f15a8 100644 --- a/lib/cassandra/protocol/v3.rb +++ b/lib/cassandra/protocol/v3.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -166,7 +166,8 @@ def actual_decode(buffer, fields, frame_length, code) if compression == 1 if @compressor buffer = CqlByteBuffer.new( - @compressor.decompress(buffer.read(frame_length))) + @compressor.decompress(buffer.read(frame_length)) + ) frame_length = buffer.size else raise Errors::DecodingError, @@ -275,7 +276,8 @@ def decode_response(opcode, protocol_version, buffer, size, trace_id) protocol_version, remaining_bytes, paging_state, - trace_id) + trace_id, + nil) else RowsResultResponse.new(nil, nil, @@ -290,9 +292,7 @@ def decode_response(opcode, protocol_version, buffer, size, trace_id) id = buffer.read_short_bytes params_metadata = Coder.read_metadata_v3(buffer).first result_metadata = nil - if protocol_version > 1 - result_metadata = Coder.read_metadata_v3(buffer).first - end + result_metadata = Coder.read_metadata_v3(buffer).first if protocol_version > 1 PreparedResultResponse.new(nil, nil, diff --git a/lib/cassandra/protocol/v4.rb b/lib/cassandra/protocol/v4.rb index b4861258e..a9afe1ce3 100644 --- a/lib/cassandra/protocol/v4.rb +++ b/lib/cassandra/protocol/v4.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ module V4 class Encoder HEADER_FORMAT = 'c2ncN'.freeze - def initialize(compressor = nil, protocol_version = 4) + def initialize(compressor, protocol_version) @compressor = compressor @protocol_version = protocol_version end @@ -31,6 +31,7 @@ def initialize(compressor = nil, protocol_version = 4) def encode(buffer, request, stream_id) flags = 0 flags |= 0x02 if request.trace? + flags |= 0x10 if @protocol_version == Cassandra::Protocol::Versions::BETA_VERSION body = CqlByteBuffer.new @@ -59,7 +60,7 @@ def write_parameters(buffer, params, types, names = EMPTY_LIST) end class Decoder - def initialize(handler, compressor = nil) + def initialize(handler, compressor = nil, custom_type_handlers = {}) @handler = handler @compressor = compressor @state = :initial @@ -68,6 +69,7 @@ def initialize(handler, compressor = nil) @code = nil @length = nil @buffer = CqlByteBuffer.new + @custom_type_handlers = custom_type_handlers end def <<(data) @@ -177,7 +179,8 @@ def actual_decode(buffer, fields, frame_length, code) if compression if @compressor buffer = CqlByteBuffer.new( - @compressor.decompress(buffer.read(frame_length))) + @compressor.decompress(buffer.read(frame_length)) + ) frame_length = buffer.size else raise Errors::DecodingError, @@ -258,15 +261,33 @@ def decode_response(opcode, buffer.read_int, (buffer.read_byte != 0)) when 0x1300 - ReadFailureErrorResponse.new(custom_payload, - warnings, - code, - message, - buffer.read_consistency, - buffer.read_int, - buffer.read_int, - buffer.read_int, - (buffer.read_byte != 0)) + cl = buffer.read_consistency + received = buffer.read_int + block_for = buffer.read_int + if protocol_version < 5 + ReadFailureErrorResponse.new(custom_payload, + warnings, + code, + message, + cl, + received, + block_for, + buffer.read_int, + (buffer.read_byte != 0), + nil) + else + failures_by_node = buffer.read_reason_map + ReadFailureErrorResponse.new(custom_payload, + warnings, + code, + message, + cl, + received, + block_for, + nil, + (buffer.read_byte != 0), + failures_by_node) + end when 0x1400 FunctionFailureErrorResponse.new(custom_payload, warnings, @@ -276,15 +297,33 @@ def decode_response(opcode, buffer.read_string, buffer.read_string_list) when 0x1500 - WriteFailureErrorResponse.new(custom_payload, - warnings, - code, - message, - buffer.read_consistency, - buffer.read_int, - buffer.read_int, - buffer.read_int, - buffer.read_string) + cl = buffer.read_consistency + received = buffer.read_int + block_for = buffer.read_int + if protocol_version < 5 + WriteFailureErrorResponse.new(custom_payload, + warnings, + code, + message, + cl, + received, + block_for, + buffer.read_int, + buffer.read_string, + nil) + else + failures_by_node = buffer.read_reason_map + WriteFailureErrorResponse.new(custom_payload, + warnings, + code, + message, + cl, + received, + block_for, + nil, + buffer.read_string, + failures_by_node) + end when 0x2400 AlreadyExistsErrorResponse.new(custom_payload, warnings, @@ -325,11 +364,12 @@ def decode_response(opcode, protocol_version, remaining_bytes, paging_state, - trace_id) + trace_id, + @custom_type_handlers) else RowsResultResponse.new(custom_payload, warnings, - Coder.read_values_v4(buffer, column_specs), + Coder.read_values_v4(buffer, column_specs, @custom_type_handlers), column_specs, paging_state, trace_id) diff --git a/lib/cassandra/reconnection.rb b/lib/cassandra/reconnection.rb index 4ddbb22dc..921dc6126 100644 --- a/lib/cassandra/reconnection.rb +++ b/lib/cassandra/reconnection.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/reconnection/policies.rb b/lib/cassandra/reconnection/policies.rb index 0e23f5e55..e4d043c7a 100644 --- a/lib/cassandra/reconnection/policies.rb +++ b/lib/cassandra/reconnection/policies.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/reconnection/policies/constant.rb b/lib/cassandra/reconnection/policies/constant.rb index 168f4c512..1a5dbc26c 100644 --- a/lib/cassandra/reconnection/policies/constant.rb +++ b/lib/cassandra/reconnection/policies/constant.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/reconnection/policies/exponential.rb b/lib/cassandra/reconnection/policies/exponential.rb index a43e7c5b4..a0d15db6a 100644 --- a/lib/cassandra/reconnection/policies/exponential.rb +++ b/lib/cassandra/reconnection/policies/exponential.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/result.rb b/lib/cassandra/result.rb index 7f7241c38..a6753c25d 100644 --- a/lib/cassandra/result.rb +++ b/lib/cassandra/result.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ def execution_info @consistency, @retries, @trace_id ? - Execution::Trace.new(@trace_id, @client) : + Execution::Trace.new(@trace_id, @client, @options.load_balancing_policy) : nil) end @@ -76,8 +76,8 @@ def next_page(options = nil) # # @note `:paging_state` option will be ignored. # - # @return [Cassandra::Future] `nil` if last - # page + # @return [Cassandra::Future] a future that resolves to a new Result if there is a new page, + # `nil` otherwise. # # @see Cassandra::Session#execute def next_page_async(options = nil) @@ -96,7 +96,8 @@ def next_page_async(options = nil) # undefined and will likely cause a server process of the coordinator of # such request to abort. # - # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v2.spec#L482-L487 Paging State description in Cassandra Native Protocol v2 specification + # @see https://github.com/apache/cassandra/blob/cassandra-2.0.16/doc/native_protocol_v2.spec#L482-L487 Paging State + # description in Cassandra Native Protocol v2 specification def paging_state end end diff --git a/lib/cassandra/retry.rb b/lib/cassandra/retry.rb index dc308529b..61761f9c0 100644 --- a/lib/cassandra/retry.rb +++ b/lib/cassandra/retry.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/retry/policies.rb b/lib/cassandra/retry/policies.rb index b58bfd1ef..bf83eeea1 100644 --- a/lib/cassandra/retry/policies.rb +++ b/lib/cassandra/retry/policies.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/retry/policies/default.rb b/lib/cassandra/retry/policies/default.rb index 905fb352d..6853f2b92 100644 --- a/lib/cassandra/retry/policies/default.rb +++ b/lib/cassandra/retry/policies/default.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/retry/policies/downgrading_consistency.rb b/lib/cassandra/retry/policies/downgrading_consistency.rb index 81d81bc72..e45a2249a 100644 --- a/lib/cassandra/retry/policies/downgrading_consistency.rb +++ b/lib/cassandra/retry/policies/downgrading_consistency.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,9 +24,7 @@ class DowngradingConsistency def read_timeout(statement, consistency, required, received, retrieved, retries) return reraise if retries > 0 || SERIAL_CONSISTENCIES.include?(consistency) - if received < required - return max_likely_to_work(consistency, required, received) - end + return max_likely_to_work(consistency, required, received) if received < required retrieved ? reraise : try_again(consistency) end diff --git a/lib/cassandra/retry/policies/fallthrough.rb b/lib/cassandra/retry/policies/fallthrough.rb index 95bd90edd..47d15fc54 100644 --- a/lib/cassandra/retry/policies/fallthrough.rb +++ b/lib/cassandra/retry/policies/fallthrough.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/session.rb b/lib/cassandra/session.rb index cf9427de9..23a39d231 100644 --- a/lib/cassandra/session.rb +++ b/lib/cassandra/session.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,10 +29,11 @@ class Session def_delegators :@client, :keyspace # @private - def initialize(client, default_options, futures_factory) + def initialize(client, default_options, futures_factory, profile_manager) @client = client @options = default_options @futures = futures_factory + @profile_manager = profile_manager end # Executes a given statement and returns a future result @@ -66,6 +67,8 @@ def initialize(client, default_options, futures_factory) # statement can be retried safely on timeout. # @option options [Hash<[String, Symbol], String>] :payload (nil) custom # outgoing payload to be sent with the request. + # @option options [String, Symbol] :execution_profile (nil) name of {Cassandra::Execution::Profile} + # from which to obtain certain query options. Defaults to the cluster's default execution profile. # # @see Cassandra.cluster Options that can be specified on the cluster-level # and their default values. @@ -78,31 +81,19 @@ def initialize(client, default_options, futures_factory) # # @return [Cassandra::Future] # - # @see Cassandra::Session#execute A list of errors this future can be - # resolved with + # @see Cassandra::Session#execute A list of errors this future can be resolved with def execute_async(statement, options = nil) - options = if options - @options.override(options) - else - @options - end + options = merge_execution_options(options) case statement when ::String - @client.query(Statements::Simple.new(statement, - options.arguments, - options.type_hints, - options.idempotent?), - options) - when Statements::Simple - @client.query(statement, options) - when Statements::Prepared - @client.execute(statement.bind(options.arguments), options) - when Statements::Bound - @client.execute(statement, options) - when Statements::Batch - Util.assert_not_empty(statement.statements) { 'batch cannot be empty' } - @client.batch(statement, options) + Statements::Simple.new(statement, + options.arguments, + options.type_hints, + options.idempotent?).accept(@client, + options) + when Statement + statement.accept(@client, options) else @futures.error(::ArgumentError.new("unsupported statement #{statement.inspect}")) end @@ -117,7 +108,7 @@ def execute_async(statement, options = nil) # Cassandra::Statements::Bound, Cassandra::Statements::Prepared] # statement to execute # - # @param options [Hash] (nil) a customizable set of options + # @param options [Hash] (nil) a customizable set of options. See {#execute_async} for details. # # @see Cassandra::Session#execute_async # @see Cassandra::Future#get @@ -145,15 +136,13 @@ def execute(statement, options = nil) # @option options [Boolean] :idempotent (false) specify whether the # statement being prepared can be retried safely on timeout during # execution. + # @option options [String, Symbol] :execution_profile (nil) name of {Cassandra::Execution::Profile} + # from which to obtain certain query options. Defaults to the cluster's default execution profile. # # @return [Cassandra::Future] future # prepared statement def prepare_async(statement, options = nil) - options = if options.is_a?(::Hash) - @options.override(options) - else - @options - end + options = merge_execution_options(options) case statement when ::String @@ -168,14 +157,20 @@ def prepare_async(statement, options = nil) end # A blocking wrapper around {Cassandra::Session#prepare_async} + # + # @param statement [String, Cassandra::Statements::Simple] a statement to + # prepare + # + # @param options [Hash] (nil) a customizable set of options. See {#prepare_async} for details. + # # @see Cassandra::Session#prepare_async # @see Cassandra::Future#get # # @return [Cassandra::Statements::Prepared] prepared statement # @raise [Cassandra::Errors::NoHostsAvailable] if none of the hosts can be reached # @raise [Cassandra::Errors::ExecutionError] if Cassandra returns an error response - def prepare(*args) - prepare_async(*args).get + def prepare(statement, options = nil) + prepare_async(statement, options).get end # Returns a logged {Statements::Batch} instance and optionally yields it to @@ -238,7 +233,34 @@ def close # @private def inspect - "#<#{self.class.name}:0x#{object_id.to_s(16)}>" + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "@keyspace=#{keyspace.inspect}, " \ + "@options=#{@options.inspect}>" + end + + private + + # @private + def merge_execution_options(options) + if options + Util.assert_instance_of(::Hash, options) + # Yell if the caller gave us a bad profile name. + execution_profile = nil + if options.key?(:execution_profile) + execution_profile = @profile_manager.profiles[options[:execution_profile]] + raise ::ArgumentError.new("Unknown execution profile #{options[:execution_profile]}") unless execution_profile + end + + # This looks a little hokey, so let's explain: Execution::Options.override takes a + # varargs-style array of things to merge into the base options object (to produce + # a new Options object, not mutate the base). If an execution profile was specified, + # we want its attributes to override the base options. In addition, if individual options + # were specified, we want *those* to take precedence over the execution profile attributes. + # So we override in this order. + @options.override(execution_profile, options) + else + @options + end end end end diff --git a/lib/cassandra/statement.rb b/lib/cassandra/statement.rb index 785e17f53..528240b47 100644 --- a/lib/cassandra/statement.rb +++ b/lib/cassandra/statement.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,5 +23,10 @@ module Statement def idempotent? !!@idempotent end + + # @private + def accept(client, options) + raise NotImplementedError, "#{self.class} must implement :accept method" + end end end diff --git a/lib/cassandra/statements.rb b/lib/cassandra/statements.rb index 47f31e547..ca4a4f145 100644 --- a/lib/cassandra/statements.rb +++ b/lib/cassandra/statements.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/statements/batch.rb b/lib/cassandra/statements/batch.rb index c40ae9442..1a022c8cf 100644 --- a/lib/cassandra/statements/batch.rb +++ b/lib/cassandra/statements/batch.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -113,6 +113,12 @@ def add(statement, options = nil) self end + # @private + def accept(client, options) + Util.assert_not_empty(statements) { 'batch cannot be empty' } + client.batch(self, options) + end + # Determines whether or not the statement is safe to retry on timeout # Batches are idempotent only when all statements in a batch are. # @return [Boolean] whether the statement is safe to retry on timeout diff --git a/lib/cassandra/statements/bound.rb b/lib/cassandra/statements/bound.rb index 4e4f7c875..d8ac2bc34 100644 --- a/lib/cassandra/statements/bound.rb +++ b/lib/cassandra/statements/bound.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,15 +28,19 @@ class Bound attr_reader :params # @private attr_reader :params_types, :result_metadata, :keyspace, :partition_key + # @private prepared-statement id + attr_reader :id # @private - def initialize(cql, + def initialize(id, + cql, params_types, result_metadata, params, keyspace = nil, partition_key = nil, idempotent = false) + @id = id @cql = cql @params_types = params_types @result_metadata = result_metadata @@ -46,6 +50,11 @@ def initialize(cql, @idempotent = idempotent end + # @private + def accept(client, options) + client.execute(self, options) + end + # @return [String] a CLI-friendly bound statement representation def inspect "#<#{self.class.name}:0x#{object_id.to_s(16)} @cql=#{@cql.inspect} " \ diff --git a/lib/cassandra/statements/prepared.rb b/lib/cassandra/statements/prepared.rb index 2c37d0753..bf9982a89 100644 --- a/lib/cassandra/statements/prepared.rb +++ b/lib/cassandra/statements/prepared.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,9 +27,12 @@ class Prepared attr_reader :cql # @private attr_reader :result_metadata + # @private prepared-statement id + attr_reader :id # @private - def initialize(payload, + def initialize(id, + payload, warnings, cql, params_metadata, @@ -44,6 +47,7 @@ def initialize(payload, retries, client, connection_options) + @id = id @payload = payload @warnings = warnings @cql = cql @@ -131,7 +135,8 @@ def bind(args = nil) partition_key = create_partition_key(params) - Bound.new(@cql, + Bound.new(@id, + @cql, param_types, @result_metadata, params, @@ -151,10 +156,15 @@ def execution_info @consistency, @retries, @trace_id ? - Execution::Trace.new(@trace_id, @client) : + Execution::Trace.new(@trace_id, @client, @options.load_balancing_policy) : nil) end + # @private + def accept(client, options) + client.execute(bind(options.arguments), options) + end + # @return [String] a CLI-friendly prepared statement representation def inspect "#<#{self.class.name}:0x#{object_id.to_s(16)} @cql=#{@cql.inspect}>" @@ -180,7 +190,9 @@ def create_partition_key(values) 'the partition key and must be present.' end - if @connection_options.protocol_version >= 3 + if @connection_options.protocol_version >= 4 + Protocol::Coder.write_value_v4(buffer, value, type) + elsif @connection_options.protocol_version >= 3 Protocol::Coder.write_value_v3(buffer, value, type) else Protocol::Coder.write_value_v1(buffer, value, type) @@ -189,9 +201,9 @@ def create_partition_key(values) buffer.discard(4) # discard size else buf = Protocol::CqlByteBuffer.new - partition_key.each do |i| - value = values[i] - metadata = params_metadata[i] + partition_key.each do |ind| + value = values[ind] + metadata = params_metadata[ind] name = metadata[2] type = metadata[3] @@ -200,7 +212,9 @@ def create_partition_key(values) 'the partition key and must be present.' end - if @connection_options.protocol_version >= 3 + if @connection_options.protocol_version >= 4 + Protocol::Coder.write_value_v4(buf, value, type) + elsif @connection_options.protocol_version >= 3 Protocol::Coder.write_value_v3(buf, value, type) else Protocol::Coder.write_value_v1(buf, value, type) diff --git a/lib/cassandra/statements/simple.rb b/lib/cassandra/statements/simple.rb index d0dcd8b4b..aafc4bdaf 100644 --- a/lib/cassandra/statements/simple.rb +++ b/lib/cassandra/statements/simple.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -93,6 +93,11 @@ def initialize(cql, params = nil, type_hints = nil, idempotent = false) @idempotent = idempotent end + # @private + def accept(client, options) + client.query(self, options) + end + # @return [String] a CLI-friendly simple statement representation def inspect "#<#{self.class.name}:0x#{object_id.to_s(16)} @cql=#{@cql.inspect} " \ diff --git a/lib/cassandra/statements/void.rb b/lib/cassandra/statements/void.rb index f4435e82b..33a887c1c 100644 --- a/lib/cassandra/statements/void.rb +++ b/lib/cassandra/statements/void.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,11 @@ module Statements class Void include Statement + # @private + def accept(client, options) + nil + end + # Returns nothing # @return [nil] there is no cql for the void statement def cql diff --git a/lib/cassandra/table.rb b/lib/cassandra/table.rb index 050c4f32b..5fecb09a4 100644 --- a/lib/cassandra/table.rb +++ b/lib/cassandra/table.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,241 +20,134 @@ module Cassandra # Represents a cassandra table # @see Cassandra::Keyspace#each_table # @see Cassandra::Keyspace#table - class Table - # @private - class Options - attr_reader :comment, :read_repair_chance, :local_read_repair_chance, - :gc_grace_seconds, :caching, :bloom_filter_fp_chance, - :populate_io_cache_on_flush, :memtable_flush_period_in_ms, - :default_time_to_live, :speculative_retry, :index_interval, - :replicate_on_write, :compaction_strategy, :compact_storage, - :compression_parameters - - def initialize(comment, - read_repair_chance, - local_read_repair_chance, - gc_grace_seconds, - caching, - bloom_filter_fp_chance, - populate_io_cache_on_flush, - memtable_flush_period_in_ms, - default_time_to_live, - speculative_retry, - index_interval, - replicate_on_write, - min_index_interval, - max_index_interval, - compaction_strategy, - compression_parameters, - compact_storage) - @comment = comment - @read_repair_chance = read_repair_chance - @local_read_repair_chance = local_read_repair_chance - @gc_grace_seconds = gc_grace_seconds - @caching = caching - @bloom_filter_fp_chance = bloom_filter_fp_chance - @populate_io_cache_on_flush = populate_io_cache_on_flush - @memtable_flush_period_in_ms = memtable_flush_period_in_ms - @default_time_to_live = default_time_to_live - @speculative_retry = speculative_retry - @index_interval = index_interval - @replicate_on_write = replicate_on_write - @min_index_interval = min_index_interval - @max_index_interval = max_index_interval - @compaction_strategy = compaction_strategy - @compression_parameters = compression_parameters - @compact_storage = compact_storage - end - - def replicate_on_write? - @replicate_on_write - end - - def populate_io_cache_on_flush? - @populate_io_cache_on_flush - end - - def compact_storage? - @compact_storage - end - - def to_cql - options = [] - - options << 'COMPACT STORAGE' if @compact_storage - unless @bloom_filter_fp_chance.nil? - options << - "bloom_filter_fp_chance = #{Util.encode_object(@bloom_filter_fp_chance)}" - end - options << "caching = #{Util.encode_object(@caching)}" unless @caching.nil? - options << "comment = #{Util.encode_object(@comment)}" unless @comment.nil? - unless @compaction_strategy.nil? - options << "compaction = #{@compaction_strategy.to_cql}" - end - unless @compression_parameters.nil? - options << "compression = #{Util.encode_object(@compression_parameters)}" - end - unless @local_read_repair_chance.nil? - options << 'dclocal_read_repair_chance = ' \ - "#{Util.encode_object(@local_read_repair_chance)}" - end - unless @default_time_to_live.nil? - options << "default_time_to_live = #{Util.encode_object(@default_time_to_live)}" - end - unless @gc_grace_seconds.nil? - options << "gc_grace_seconds = #{Util.encode_object(@gc_grace_seconds)}" - end - unless @index_interval.nil? - options << "index_interval = #{Util.encode_object(@index_interval)}" - end - unless @max_index_interval.nil? - options << "max_index_interval = #{Util.encode_object(@max_index_interval)}" - end - unless @memtable_flush_period_in_ms.nil? - options << 'memtable_flush_period_in_ms = ' \ - "#{Util.encode_object(@memtable_flush_period_in_ms)}" - end - unless @min_index_interval.nil? - options << "min_index_interval = #{Util.encode_object(@min_index_interval)}" - end - unless @populate_io_cache_on_flush.nil? - options << "populate_io_cache_on_flush = '#{@populate_io_cache_on_flush}'" - end - unless @read_repair_chance.nil? - options << "read_repair_chance = #{Util.encode_object(@read_repair_chance)}" - end - unless @replicate_on_write.nil? - options << "replicate_on_write = '#{@replicate_on_write}'" - end - unless @speculative_retry.nil? - options << "speculative_retry = #{Util.encode_object(@speculative_retry)}" - end - - options.join("\nAND ") - end - - def eql?(other) - other.is_a?(Options) && - @comment == other.comment && - @read_repair_chance == other.read_repair_chance && - @local_read_repair_chance == other.local_read_repair_chance && - @gc_grace_seconds == other.gc_grace_seconds && - @caching == other.caching && - @bloom_filter_fp_chance == other.bloom_filter_fp_chance && - @populate_io_cache_on_flush == other.populate_io_cache_on_flush && - @memtable_flush_period_in_ms == other.memtable_flush_period_in_ms && - @default_time_to_live == other.default_time_to_live && - @speculative_retry == other.speculative_retry && - @index_interval == other.index_interval && - @replicate_on_write == other.replicate_on_write && - @compaction_strategy == other.compaction_strategy && - @compression_parameters == other.compression_parameters && - @compact_storage == other.compact_storage - end - alias == eql? - end + class Table < ColumnContainer + # @return [Array] an array of order values (`:asc` or `:desc`) that apply to the + # `clustering_columns` array. + attr_reader :clustering_order # @private - class Compaction - attr_reader :klass, :options + def initialize(keyspace, + name, + partition_key, + clustering_columns, + other_columns, + options, + clustering_order, + id) + super(keyspace, name, partition_key, clustering_columns, other_columns, options, id) + @clustering_order = clustering_order.freeze + @indexes = [] + @indexes_hash = {} + @materialized_views = [] + @materialized_views_hash = {} + @triggers = [] + @triggers_hash = {} + end - def initialize(klass, options) - @klass = klass - @options = options - end + # @param name [String] index name + # @return [Boolean] whether this table has a given index + def has_index?(name) + @indexes_hash.key?(name) + end - def to_cql - compaction = {'class' => @klass} - compaction.merge!(@options) + # @param name [String] index name + # @return [Cassandra::Index, nil] an index or nil + def index(name) + @indexes_hash[name] + end - Util.encode_hash(compaction) + # Yield or enumerate each index bound to this table + # @overload each_index + # @yieldparam index [Cassandra::Index] current index + # @return [Cassandra::Table] self + # @overload each_index + # @return [Array] a list of indexes + def each_index(&block) + if block_given? + @indexes.each(&block) + self + else + @indexes.freeze end + end + alias indexes each_index - def eql?(other) - other.is_a?(Compaction) && - @klass == other.klass && - @options == other.options - end - alias == eql? + # @param name [String] trigger name + # @return [Boolean] whether this table has a given trigger + def has_trigger?(name) + @triggers_hash.key?(name) end - # @private - attr_reader :keyspace - # @return [String] table name - attr_reader :name - # @private - attr_reader :options - # @private - attr_reader :partition_key + # @param name [String] trigger name + # @return [Cassandra::Trigger, nil] a trigger or nil + def trigger(name) + @triggers_hash[name] + end - # @private - def initialize(keyspace, - name, - partition_key, - clustering_columns, - columns, - options, - clustering_order) - @keyspace = keyspace - @name = name - @partition_key = partition_key - @clustering_columns = clustering_columns - @columns = columns - @options = options - @clustering_order = clustering_order + # Yield or enumerate each trigger bound to this table + # @overload each_trigger + # @yieldparam trigger [Cassandra::Index] current trigger + # @return [Cassandra::Table] self + # @overload each_trigger + # @return [Array] a list of triggers + def each_trigger(&block) + if block_given? + @triggers.each(&block) + self + else + @triggers.freeze + end end + alias triggers each_trigger - # @param name [String] column name - # @return [Boolean] whether this table has a given column - def has_column?(name) - @columns.key?(name) + # @param name [String] materialized view name + # @return [Boolean] whether this table has a given materialized view + def has_materialized_view?(name) + @materialized_views_hash.key?(name) end - # @param name [String] column name - # @return [Cassandra::Column, nil] a column or nil - def column(name) - @columns[name] + # @param name [String] materialized view name + # @return [Cassandra::MaterializedView, nil] a materialized view or nil + def materialized_view(name) + @materialized_views_hash[name] end - # Yield or enumerate each column defined in this table - # @overload each_column - # @yieldparam column [Cassandra::Column] current column + # Yield or enumerate each materialized view bound to this table + # @overload each_materialized_view + # @yieldparam materialized_view [Cassandra::MaterializedView] current materialized view # @return [Cassandra::Table] self - # @overload each_column - # @return [Array] a list of columns - def each_column(&block) + # @overload each_materialized_view + # @return [Array] a list of materialized views + def each_materialized_view(&block) if block_given? - @columns.each_value(&block) + @materialized_views.each(&block) self else - @columns.values + @materialized_views.freeze end end - alias columns each_column + alias materialized_views each_materialized_view # @return [String] a cql representation of this table def to_cql - cql = "CREATE TABLE #{Util.escape_name(@keyspace)}.#{Util.escape_name(@name)} (\n" - primary_key = nil - if @partition_key.one? && @clustering_columns.empty? - primary_key = @partition_key.first.name - end + cql = "CREATE TABLE #{Util.escape_name(@keyspace.name)}.#{Util.escape_name(@name)} (\n" + primary_key = @partition_key.first.name if @partition_key.one? && @clustering_columns.empty? first = true - @columns.each do |(_, column)| + @columns.each do |column| if first first = false else - cql << ",\n" unless first + cql << ",\n" end - cql << " #{column.name} #{type_to_cql(column.type, column.frozen?)}" + cql << " #{Util.escape_name(column.name)} #{type_to_cql(column.type, column.frozen?)}" cql << ' PRIMARY KEY' if primary_key && column.name == primary_key end unless primary_key cql << ",\n PRIMARY KEY (" if @partition_key.one? - cql << @partition_key.first.name + cql << Util.escape_name(@partition_key.first.name) else cql << '(' first = true @@ -264,12 +157,12 @@ def to_cql else cql << ', ' end - cql << column.name + cql << Util.escape_name(column.name) end cql << ')' end @clustering_columns.each do |column| - cql << ", #{column.name}" + cql << ", #{Util.escape_name(column.name)}" end cql << ')' end @@ -285,7 +178,7 @@ def to_cql else cql << ', ' end - cql << "#{column.name} #{order.to_s.upcase}" + cql << "#{Util.escape_name(column.name)} #{order.to_s.upcase}" end cql << ")\n AND " end @@ -296,21 +189,29 @@ def to_cql end # @private - def inspect - "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ - "@keyspace=#{@keyspace} @name=#{@name}>" + def add_index(index) + @indexes << index + @indexes_hash[index.name] = index + end + + # @private + def add_view(view) + @materialized_views << view + @materialized_views_hash[view.name] = view + end + + # @private + def add_trigger(trigger) + @triggers << trigger + @triggers_hash[trigger.name] = trigger end # @private def eql?(other) other.is_a?(Table) && - @keyspace == other.keyspace && - @name == other.name && - @partition_key == other.partition_key && - @clustering_columns == other.clustering_columns && - @columns == other.raw_columns && - @options == other.options && - @clustering_order == other.clustering_order + super.eql?(other) && + @clustering_order == other.clustering_order && + @indexes == other.indexes end alias == eql? @@ -322,7 +223,7 @@ def type_to_cql(type, is_frozen) when :tuple "frozen <#{type}>" when :udt - if @keyspace == type.keyspace + if @keyspace.name == type.keyspace "frozen <#{Util.escape_name(type.name)}>" else "frozen <#{Util.escape_name(type.keyspace)}.#{Util.escape_name(type.name)}>" @@ -335,15 +236,5 @@ def type_to_cql(type, is_frozen) end end end - - attr_reader :clustering_columns, :clustering_order - protected :clustering_columns, :clustering_order - - protected - - # @private - def raw_columns - @columns - end end end diff --git a/lib/cassandra/time.rb b/lib/cassandra/time.rb index 602baa07e..d49105b98 100644 --- a/lib/cassandra/time.rb +++ b/lib/cassandra/time.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/cassandra/time_uuid.rb b/lib/cassandra/time_uuid.rb index 4539ad9e0..0613dfdd4 100644 --- a/lib/cassandra/time_uuid.rb +++ b/lib/cassandra/time_uuid.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,11 @@ module Cassandra class TimeUuid < Uuid include Comparable + # @private + LOWER_HALF_MASK = 0xffffffff_ffffffff + # @private + GREGORIAN_OFFSET = 122192928000000000 + # Returns the time component from this UUID as a Time. # # @return [Time] @@ -69,12 +74,5 @@ def time_bits t |= (n & 0xffffffff00000000) >> 32 t end - - private - - # @private - LOWER_HALF_MASK = 0xffffffff_ffffffff - # @private - GREGORIAN_OFFSET = 122192928000000000 end end diff --git a/lib/cassandra/timestamp_generator.rb b/lib/cassandra/timestamp_generator.rb new file mode 100644 index 000000000..8b97a6d59 --- /dev/null +++ b/lib/cassandra/timestamp_generator.rb @@ -0,0 +1,37 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + # A generator is used to create client-timestamps (in the form of long integers) to send with C* requests when + # the `:client_timestamps` cluster option is set to true. + # + # @abstract A timestamp generator given to {Cassandra.cluster} doesn't need to include this module, but needs to + # implement the same methods. This module exists only for documentation purposes. + module TimestampGenerator + # Create a new timestamp, as a 64-bit integer. Calls must return monotonically increasing values. + # + # @return [Integer] an integer representing a timestamp in microseconds. + # @raise [NotImplementedError] if a class including this module does not define this method. + def next + raise NotImplementedError, "#{self.class} class must implement the 'next' method" + end + end +end + +require 'cassandra/timestamp_generator/ticking_on_duplicate' +require 'cassandra/timestamp_generator/simple' diff --git a/lib/cassandra/timestamp_generator/simple.rb b/lib/cassandra/timestamp_generator/simple.rb new file mode 100644 index 000000000..045676870 --- /dev/null +++ b/lib/cassandra/timestamp_generator/simple.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module TimestampGenerator + # Generate long integer timestamps from current time. This implementation relies on the {::Time} class to return + # microsecond precision time. + # @note It is not appropriate for use with JRuby because its {::Time#now} returns millisecond precision time. + class Simple + include TimestampGenerator + + # Create a new timestamp, as a 64-bit integer. This is just a wrapper around Time::now. + # + # @return [Integer] an integer representing a timestamp in microseconds. + def next + # Use Time.now, which has microsecond precision on MRI (and probably Rubinius) to make an int representing + # client timestamp in protocol requests. + timestamp = ::Time.now + timestamp.tv_sec * 1000000 + timestamp.tv_usec + end + end + end +end diff --git a/lib/cassandra/timestamp_generator/ticking_on_duplicate.rb b/lib/cassandra/timestamp_generator/ticking_on_duplicate.rb new file mode 100644 index 000000000..b71b2e8a0 --- /dev/null +++ b/lib/cassandra/timestamp_generator/ticking_on_duplicate.rb @@ -0,0 +1,58 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + module TimestampGenerator + # In JRuby, {::Time} has millisecond precision. We require client timestamps to have microsecond precision to + # minimize clashes in C*. This generator keeps track of the last generated timestamp, and if the current-time + # is within the same millisecond as the last, it fills the microsecond portion of the new timestamp with the + # value of an incrementing counter. + # + # For example, if the generator triggers twice at time 12345678000 (microsecond granularity, but ms precisions + # as shown by 0's for the three least-significant digits), it'll return 12345678000 and 12345678001. + class TickingOnDuplicate + include MonitorMixin + include TimestampGenerator + + # @private + def initialize + mon_initialize + @last = 0 + end + + # Create a new timestamp, as a 64-bit integer. + # + # @return [Integer] an integer representing a timestamp in microseconds. + def next + now = ::Time.now + now_millis = now.tv_sec * 1000 + now.tv_usec / 1000 + synchronize do + millis = @last / 1000 + counter = @last % 1000 + if millis >= now_millis + counter += 1 + else + millis = now_millis + counter = 0 + end + @last = millis * 1000 + counter + end + end + end + end +end diff --git a/lib/cassandra/trigger.rb b/lib/cassandra/trigger.rb new file mode 100644 index 000000000..fc574ec32 --- /dev/null +++ b/lib/cassandra/trigger.rb @@ -0,0 +1,67 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +module Cassandra + # Represents a trigger on a cassandra table + class Trigger + # @return [Cassandra::Table] table that the trigger applies to. + attr_reader :table + # @return [String] name of the trigger. + attr_reader :name + # @return [Hash] options of the trigger. + attr_reader :options + + # @private + def initialize(table, + name, + options) + @table = table + @name = name.freeze + @options = options.freeze + end + + # @return [String] name of the trigger class + def custom_class_name + @options['class'] + end + + # @return [String] a cql representation of this trigger + def to_cql + keyspace_name = Util.escape_name(@table.keyspace.name) + table_name = Util.escape_name(@table.name) + trigger_name = Util.escape_name(@name) + + "CREATE TRIGGER #{trigger_name} ON #{keyspace_name}.#{table_name} USING '#{@options['class']}';" + end + + # @private + def eql?(other) + other.is_a?(Trigger) && + @table == other.table && + @name == other.name && + @options == other.options + end + alias == eql? + + # @private + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} " \ + "@name=#{@name.inspect} @table=#{@table.inspect} @options=#{@options.inspect}>" + end + end +end diff --git a/lib/cassandra/tuple.rb b/lib/cassandra/tuple.rb index 6f74fbf07..37430fb76 100644 --- a/lib/cassandra/tuple.rb +++ b/lib/cassandra/tuple.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,16 +40,12 @@ def [](i) def fetch(i) i = Integer(i) - if i < 0 || i >= @types.size - raise ::IndexError, "index #{i} is outside of tuple, size: #{@types.size}" - end + raise ::IndexError, "index #{i} is outside of tuple, size: #{@types.size}" if i < 0 || i >= @types.size @values[i] end def []=(i, value) - if i < 0 || i >= @types.size - raise ::IndexError, "index #{i} is outside of tuple, size: #{@types.size}" - end + raise ::IndexError, "index #{i} is outside of tuple, size: #{@types.size}" if i < 0 || i >= @types.size Util.assert_type(@types[i], value) @values[i] = value end @@ -59,7 +55,7 @@ def size end def inspect - "#" + "#" end end @@ -90,9 +86,7 @@ def [](i) # @return [Object] value of the tuple at position `i` def fetch(i) i = Integer(i) - if i < 0 || i >= @values.size - raise ::IndexError, "index #{i} is outside of tuple, size: #{@values.size}" - end + raise ::IndexError, "index #{i} is outside of tuple, size: #{@values.size}" if i < 0 || i >= @values.size @values[i] end @@ -103,9 +97,7 @@ def fetch(i) # @return [Object] value of the tuple at position `i` def []=(i, value) i = Integer(i) - if i < 0 || i >= @values.size - raise ::IndexError, "index #{i} is outside of tuple, size: #{@values.size}" - end + raise ::IndexError, "index #{i} is outside of tuple, size: #{@values.size}" if i < 0 || i >= @values.size @values[i] = value end @@ -122,7 +114,7 @@ def to_s # @private def inspect - "#" + "#" end # @private diff --git a/lib/cassandra/types.rb b/lib/cassandra/types.rb index afb73f90c..520ce6740 100644 --- a/lib/cassandra/types.rb +++ b/lib/cassandra/types.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,7 +48,13 @@ def to_s end end - module Types; extend self + module Types + # If we use module_function, the yard docs end up showing duplicates of all + # methods: one for self, the other as instance methods. + # + # rubocop:disable Style/ModuleFunction + extend self + # @private class Simple < Type def new(value) @@ -71,12 +77,13 @@ def hash def eql?(other) other.is_a?(Simple) && @kind == other.kind end - alias :== :eql? + + alias == eql? private def new_varchar(value) - String(value) + String(value) end def assert_varchar(value, message, &block) @@ -140,7 +147,7 @@ def assert_varint(value, message, &block) end def new_boolean(value) - !!value + !value.nil? && value != false end def assert_boolean(value, message, &block) @@ -652,7 +659,7 @@ def assert(value, message = nil, &block) # @return [String] `"list"` # @see Cassandra::Type#to_s def to_s - "list<#{@value_type.to_s}>" + "list<#{@value_type}>" end def hash @@ -667,7 +674,8 @@ def hash def eql?(other) other.is_a?(List) && @value_type == other.value_type end - alias :== :eql? + + alias == eql? end class Map < Type @@ -677,7 +685,7 @@ class Map < Type # @private def initialize(key_type, value_type) super(:map) - @key_type = key_type + @key_type = key_type @value_type = value_type end @@ -704,7 +712,7 @@ def new(*value) end result else - raise ::ArgumentError, "cannot convert #{value.inspect} to #{to_s}" + raise ::ArgumentError, "cannot convert #{value.inspect} to #{self}" end end @@ -727,7 +735,7 @@ def assert(value, message = nil, &block) # @return [String] `"map"` # @see Cassandra::Type#to_s def to_s - "map<#{@key_type.to_s}, #{@value_type.to_s}>" + "map<#{@key_type}, #{@value_type}>" end def hash @@ -745,7 +753,8 @@ def eql?(other) @key_type == other.key_type && @value_type == other.value_type end - alias :== :eql? + + alias == eql? end class Set < Type @@ -816,7 +825,7 @@ def assert(value, message = nil, &block) # @return [String] `"set"` # @see Cassandra::Type#to_s def to_s - "set<#{@value_type.to_s}>" + "set<#{@value_type}>" end def hash @@ -831,7 +840,8 @@ def hash def eql?(other) other.is_a?(Set) && @value_type == other.value_type end - alias :== :eql? + + alias == eql? end # @!parse @@ -1088,7 +1098,8 @@ def hash def eql?(other) other.is_a?(Tuple) && @members == other.members end - alias :== :eql? + + alias == eql? end # @!parse @@ -1188,7 +1199,8 @@ def eql?(other) @name == other.name && @type == other.type end - alias :== :eql? + + alias == eql? end # @return [String] keyspace where this type is defined @@ -1203,15 +1215,15 @@ def eql?(other) # @private def initialize(keyspace, name, fields) super(:udt) - @keyspace = keyspace - @name = name - @fields = fields + @keyspace = keyspace + @name = name + @fields = fields end # @param name [String] field name # @return [Boolean] whether this type has a given field def has_field?(name) - @fields.any? {|f| f.name == name} + @fields.any? { |f| f.name == name } end # Yield or enumerate each field defined in this type @@ -1228,13 +1240,14 @@ def each_field(&block) @fields.dup end end - alias :fields :each_field + + alias fields each_field # @param name [String] field name # @return [Cassandra::UserDefined::Field, nil] a field with this name or # nil def field(name) - @fields.find {|f| f.name == name} + @fields.find { |f| f.name == name } end # Coerces the value to Cassandra::UDT @@ -1290,7 +1303,7 @@ def assert(value, message = nil, &block) # @see Cassandra::Type#to_s def to_s "#{Util.escape_name(@keyspace)}.#{Util.escape_name(@name)} " \ - "{#{@fields.join(', ')}}" + "{#{@fields.join(', ')}}" end def hash @@ -1310,12 +1323,13 @@ def eql?(other) @name == other.name && @fields == other.fields end - alias :== :eql? + + alias == eql? # Output this type in CQL def to_cql - cql = "CREATE TYPE #{Util.escape_name(@keyspace)}.#{Util.escape_name(@name)} " \ - "(\n" + cql = "CREATE TYPE #{Util.escape_name(@keyspace)}.#{Util.escape_name(@name)} " \ + "(\n" first = true @fields.each do |field| @@ -1346,7 +1360,7 @@ def type_to_cql(type) "frozen <#{Util.escape_name(type.keyspace)}.#{Util.escape_name(type.name)}>" end else - "#{type}" + type.to_s end end end @@ -1375,8 +1389,8 @@ def new(*value) # @raise [ArgumentError] if the value is invalid # @return [void] def assert(value, message = nil, &block) - raise ::NotImplementedError, - "unable to assert a value for custom type: #{@name.inspect}" + Util.assert_instance_of(CustomData, value, message, &block) + Util.assert_equal(self, value.class.type, message, &block) end # @return [String] a cassandra representation of this type @@ -1396,7 +1410,8 @@ def hash def eql?(other) other.is_a?(Custom) && @name == other.name end - alias :== :eql? + + alias == eql? end # @return [Cassandra::Types::Text] text type since varchar is an alias for text @@ -1503,8 +1518,7 @@ def tinyint # @return [Cassandra::Types::List] list type def list(value_type) Util.assert_instance_of(Cassandra::Type, value_type, - "list type must be a Cassandra::Type, #{value_type.inspect} given" - ) + "list type must be a Cassandra::Type, #{value_type.inspect} given") List.new(value_type) end @@ -1514,11 +1528,9 @@ def list(value_type) # @return [Cassandra::Types::Map] map type def map(key_type, value_type) Util.assert_instance_of(Cassandra::Type, key_type, - "map key type must be a Cassandra::Type, #{key_type.inspect} given" - ) + "map key type must be a Cassandra::Type, #{key_type.inspect} given") Util.assert_instance_of(Cassandra::Type, value_type, - "map value type must be a Cassandra::Type, #{value_type.inspect} given" - ) + "map value type must be a Cassandra::Type, #{value_type.inspect} given") Map.new(key_type, value_type) end @@ -1527,8 +1539,7 @@ def map(key_type, value_type) # @return [Cassandra::Types::Set] set type def set(value_type) Util.assert_instance_of(Cassandra::Type, value_type, - "set type must be a Cassandra::Type, #{value_type.inspect} given" - ) + "set type must be a Cassandra::Type, #{value_type.inspect} given") Set.new(value_type) end @@ -1536,12 +1547,11 @@ def set(value_type) # @param members [*Cassandra::Type] types of members of this tuple # @return [Cassandra::Types::Tuple] tuple type def tuple(*members) - Util.assert_not_empty(members, "tuple must contain at least one member") + Util.assert_not_empty(members, 'tuple must contain at least one member') members.each do |member| Util.assert_instance_of(Cassandra::Type, member, - "each tuple member must be a Cassandra::Type, " \ - "#{member.inspect} given" - ) + 'each tuple member must be a Cassandra::Type, ' \ + "#{member.inspect} given") end Tuple.new(*members) @@ -1551,13 +1561,25 @@ def tuple(*members) # @example Various ways of defining the same UDT # include Cassandra::Types # - # udt('simplex', 'address', {'street' => varchar, 'city' => varchar, 'state' => varchar, 'zip' => varchar}) #=> simplex.address + # udt('simplex', 'address', {'street' => varchar, + # 'city' => varchar, + # 'state' => varchar, + # 'zip' => varchar}) #=> simplex.address # - # udt('simplex', 'address', [['street', varchar], ['city', varchar], ['state', varchar], ['zip', varchar]]) #=> simplex.address + # udt('simplex', 'address', [['street', varchar], + # ['city', varchar], + # ['state', varchar], + # ['zip', varchar]]) #=> simplex.address # - # udt('simplex', 'address', ['street', varchar], ['city', varchar], ['state', varchar], ['zip', varchar]) #=> simplex.address + # udt('simplex', 'address', ['street', varchar], + # ['city', varchar], + # ['state', varchar], + # ['zip', varchar]) #=> simplex.address # - # udt('simplex', 'address', 'street', varchar, 'city', varchar, 'state', varchar, 'zip', varchar) #=> simplex.address + # udt('simplex', 'address', 'street', varchar, + # 'city', varchar, + # 'state', varchar, + # 'zip', varchar) #=> simplex.address # @param keyspace [String] name of the keyspace that this UDT is defined in # @param name [String] name of this UDT # @param fields [Hash, @@ -1567,44 +1589,37 @@ def tuple(*members) # @return [Cassandra::Types::UserDefined] user defined type def udt(keyspace, name, *fields) keyspace = String(keyspace) - name = String(name) - fields = Array(fields.first) if fields.one? + name = String(name) + fields = Array(fields.first) if fields.one? Util.assert_not_empty(fields, - "user-defined type must contain at least one field" - ) + 'user-defined type must contain at least one field') if fields.first.is_a?(::Array) fields = fields.map do |pair| Util.assert(pair.size == 2, - "fields of a user-defined type must be an Array of name and " \ - "value pairs, #{pair.inspect} given" - ) + 'fields of a user-defined type must be an Array of name and ' \ + "value pairs, #{pair.inspect} given") Util.assert_instance_of(::String, pair[0], - "each field name for a user-defined type must be a String, " \ - "#{pair[0].inspect} given" - ) + 'each field name for a user-defined type must be a String, ' \ + "#{pair[0].inspect} given") Util.assert_instance_of(Cassandra::Type, pair[1], - "each field type for a user-defined type must be a " \ - "Cassandra::Type, #{pair[1].inspect} given" - ) + 'each field type for a user-defined type must be a ' \ + "Cassandra::Type, #{pair[1].inspect} given") UserDefined::Field.new(*pair) end else - Util.assert((fields.size % 2) == 0, - "fields of a user-defined type must be an Array of alternating " \ - "names and values pairs, #{fields.inspect} given" - ) + Util.assert(fields.size.even?, + 'fields of a user-defined type must be an Array of alternating ' \ + "names and values pairs, #{fields.inspect} given") fields = fields.each_slice(2).map do |field_name, field_type| Util.assert_instance_of(::String, field_name, - "each field name for a user-defined type must be a String, " \ - "#{field_name.inspect} given" - ) + 'each field name for a user-defined type must be a String, ' \ + "#{field_name.inspect} given") Util.assert_instance_of(Cassandra::Type, field_type, - "each field type for a user-defined type must be a " \ - "Cassandra::Type, #{field_type.inspect} given" - ) + 'each field type for a user-defined type must be a ' \ + "Cassandra::Type, #{field_type.inspect} given") UserDefined::Field.new(field_name, field_type) end diff --git a/lib/cassandra/udt.rb b/lib/cassandra/udt.rb index c59f11c5e..27f0aacca 100644 --- a/lib/cassandra/udt.rb +++ b/lib/cassandra/udt.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -181,34 +181,56 @@ def eql?(other) # @param values [Hash, Array>, # *Object, *Array] - UDT field values # @example Various ways of creating the same UDT instance - # Cassandra::UDT.new({'street' => '123 Main St.', 'city' => 'Whatever', 'state' => 'XZ', 'zip' => '10020'}) + # Cassandra::UDT.new({'street' => '123 Main St.', + # 'city' => 'Whatever', + # 'state' => 'XZ', + # 'zip' => '10020'}) # - # Cassandra::UDT.new(street: '123 Main St.', city: 'Whatever', state: 'XZ', zip: '10020') + # Cassandra::UDT.new(street: '123 Main St.', + # city: 'Whatever', + # state: 'XZ', + # zip: '10020') # - # Cassandra::UDT.new('street', '123 Main St.', 'city', 'Whatever', 'state', 'XZ', 'zip', '10020') + # Cassandra::UDT.new('street', '123 Main St.', + # 'city', 'Whatever', + # 'state', 'XZ', + # 'zip', '10020') # - # Cassandra::UDT.new(:street, '123 Main St.', :city, 'Whatever', :state, 'XZ', :zip, '10020') + # Cassandra::UDT.new(:street, '123 Main St.', + # :city, 'Whatever', + # :state, 'XZ', + # :zip, '10020') # - # Cassandra::UDT.new(['street', '123 Main St.'], ['city', 'Whatever'], ['state', 'XZ'], ['zip', '10020']) + # Cassandra::UDT.new(['street', '123 Main St.'], + # ['city', 'Whatever'], + # ['state', 'XZ'], + # ['zip', '10020']) # - # Cassandra::UDT.new([:street, '123 Main St.'], [:city, 'Whatever'], [:state, 'XZ'], [:zip, '10020']) + # Cassandra::UDT.new([:street, '123 Main St.'], + # [:city, 'Whatever'], + # [:state, 'XZ'], + # [:zip, '10020']) # - # Cassandra::UDT.new([['street', '123 Main St.'], ['city', 'Whatever'], ['state', 'XZ'], ['zip', '10020']]) + # Cassandra::UDT.new([['street', '123 Main St.'], + # ['city', 'Whatever'], + # ['state', 'XZ'], + # ['zip', '10020']]) # - # Cassandra::UDT.new([[:street, '123 Main St.'], [:city, 'Whatever'], [:state, 'XZ'], [:zip, '10020']]) + # Cassandra::UDT.new([[:street, '123 Main St.'], + # [:city, 'Whatever'], + # [:state, 'XZ'], + # [:zip, '10020']]) def initialize(*values) values = Array(values.first) if values.one? Util.assert_not_empty(values, - 'user-defined type must contain at least one value' - ) + 'user-defined type must contain at least one value') if values.first.is_a?(::Array) @values = values.map do |pair| Util.assert(pair.size == 2, 'values of a user-defined type must be an Array of name and ' \ - "value pairs, #{pair.inspect} given" - ) + "value pairs, #{pair.inspect} given") name, value = pair [String(name), value] @@ -216,8 +238,7 @@ def initialize(*values) else Util.assert(values.size.even?, 'values of a user-defined type must be an Array of alternating ' \ - "names and values pairs, #{values.inspect} given" - ) + "names and values pairs, #{values.inspect} given") @values = values.each_slice(2).map do |(name, value)| [String(name), value] end @@ -313,9 +334,7 @@ def [](field) def fetch(field) case field when ::Integer - if field >= 0 && field < @values.size - raise ::IndexError, "Field index #{field.inspect} is not present" - end + raise ::IndexError, "Field index #{field.inspect} is not present" if field >= 0 && field < @values.size @values[field][1] when ::String @@ -361,9 +380,7 @@ def has_field?(field) def []=(field, value) case field when ::Integer - if field < 0 || field >= @values.size - raise ::IndexError, "Field index #{field.inspect} is not present" - end + raise ::IndexError, "Field index #{field.inspect} is not present" if field < 0 || field >= @values.size @values[field][1] = value when ::String diff --git a/lib/cassandra/util.rb b/lib/cassandra/util.rb index 687a51ab8..14b97ffd6 100644 --- a/lib/cassandra/util.rb +++ b/lib/cassandra/util.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -162,16 +162,20 @@ def encode_tuple(tuple, io = StringIO.new) end def escape_name(name) - return name if name[LOWERCASE_REGEXP] == name - DBL_QUOT + name + DBL_QUOT + # If name only contains lower-case chars and it's not a reserved word, return it + # as-is. Otherwise, quote. + return name if name[LOWERCASE_REGEXP] == name && !RESERVED_WORDS.include?(name) + + # Replace double-quotes within name with two double-quotes (if any) and surround the whole + # thing with double-quotes + DBL_QUOT + name.gsub('"', '""') + DBL_QUOT end def guess_type(object) case object when ::String then Types.varchar - when ::Fixnum then Types.bigint + when ::Integer then object.size > 8 ? Types.varint : Types.bigint when ::Float then Types.double - when ::Bignum then Types.varint when ::BigDecimal then Types.decimal when ::TrueClass then Types.boolean when ::FalseClass then Types.boolean @@ -191,6 +195,7 @@ def guess_type(object) Types.udt(object.keyspace, object.name, object.types) when UDT Types.udt('unknown', 'unknown', object.map {|k, v| [k, guess_type(v)]}) + when Cassandra::CustomData then object.class.type else raise ::ArgumentError, "Unable to guess the type of the argument: #{object.inspect}" @@ -327,5 +332,133 @@ def assert_equal(expected, actual, message = nil, &block) PRN_OPN = '('.freeze # @private PRN_CLS = ')'.freeze + RESERVED_WORDS = Set.new(%w( + add + aggregate + all + allow + alter + and + apply + as + asc + ascii + authorize + batch + begin + bigint + blob + boolean + by + called + clustering + columnfamily + compact + contains + count + counter + create + custom + date + decimal + delete + desc + describe + distinct + double + drop + entries + execute + exists + filtering + finalfunc + float + from + frozen + full + function + functions + grant + if + in + index + inet + infinity + initcond + input + insert + int + into + is + json + key + keys + keyspace + keyspaces + language + limit + list + login + map + materialized + modify + nan + nologin + norecursive + nosuperuser + not + null + of + on + options + or + order + password + permission + permissions + primary + rename + replace + returns + revoke + role + roles + schema + select + set + sfunc + smallint + static + storage + stype + superuser + table + text + time + timestamp + timeuuid + tinyint + to + token + trigger + truncate + ttl + tuple + type + unlogged + update + use + user + users + using + uuid + values + varchar + varint + view + where + with + writetime + )).freeze end end diff --git a/lib/cassandra/uuid.rb b/lib/cassandra/uuid.rb index e785efa8c..c46a31437 100644 --- a/lib/cassandra/uuid.rb +++ b/lib/cassandra/uuid.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,6 +25,13 @@ module Cassandra # If you want to generate UUIDs see {Cassandra::Uuid::Generator}. # class Uuid + # @private + RAW_FORMAT = '%032x'.force_encoding(Encoding::ASCII).freeze + # @private + HYPHEN = '-'.force_encoding(Encoding::ASCII).freeze + # @private + EMPTY_STRING = ''.freeze + # Creates a new UUID either from a string (expected to be on the standard 8-4-4-4-12 # form, or just 32 characters without hyphens), or from a 128 bit number. # @@ -78,15 +85,6 @@ def eql?(other) end alias == eql? - private - - # @private - RAW_FORMAT = '%032x'.force_encoding(Encoding::ASCII).freeze - # @private - HYPHEN = '-'.force_encoding(Encoding::ASCII).freeze - # @private - EMPTY_STRING = ''.freeze - if RUBY_ENGINE == 'jruby' # @private HEX_RE = /^[A-Fa-f0-9]+$/ @@ -94,21 +92,15 @@ def eql?(other) # @private def from_s(str) str = str.gsub(HYPHEN, EMPTY_STRING) - unless str.length == 32 - raise ::ArgumentError, "Expected 32 hexadecimal digits but got #{str.length}" - end - unless str =~ HEX_RE - raise ::ArgumentError, "invalid value for Integer(): \"#{str}\"" - end + raise ::ArgumentError, "Expected 32 hexadecimal digits but got #{str.length}" unless str.length == 32 + raise ::ArgumentError, "invalid value for Integer(): \"#{str}\"" unless str =~ HEX_RE Integer(str, 16) end else # @private def from_s(str) str = str.gsub(HYPHEN, EMPTY_STRING) - unless str.length == 32 - raise ::ArgumentError, "Expected 32 hexadecimal digits but got #{str.length}" - end + raise ::ArgumentError, "Expected 32 hexadecimal digits but got #{str.length}" unless str.length == 32 Integer(str, 16) end end diff --git a/lib/cassandra/uuid/generator.rb b/lib/cassandra/uuid/generator.rb index 605b8af2c..3b5eb037e 100644 --- a/lib/cassandra/uuid/generator.rb +++ b/lib/cassandra/uuid/generator.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,9 +49,10 @@ def initialize(node_id = (::SecureRandom.random_number(2**47) | 0x010000000000), clock = ::Time) raise ::ArgumentError, 'invalid clock' unless clock.respond_to?(:now) - @node_id = Integer(node_id) - @clock_id = Integer(clock_id) - @clock = clock + @node_id = Integer(node_id) + @clock_id = Integer(clock_id) + @clock = clock + @last_usecs = nil end # Returns a new UUID with a time component that is the current time. @@ -82,6 +83,7 @@ def initialize(node_id = (::SecureRandom.random_number(2**47) | 0x010000000000), def now now = @clock.now usecs = now.to_i * 1_000_000 + now.usec + if @last_usecs && @last_usecs - @sequence <= usecs && usecs <= @last_usecs @sequence += 1 elsif @last_usecs && @last_usecs > usecs @@ -90,6 +92,7 @@ def now else @sequence = 0 end + @last_usecs = usecs + @sequence from_usecs(@last_usecs) end diff --git a/lib/cassandra/version.rb b/lib/cassandra/version.rb index 0feb69088..df2fd7988 100644 --- a/lib/cassandra/version.rb +++ b/lib/cassandra/version.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,5 +17,5 @@ #++ module Cassandra - VERSION = '3.0.0.rc.1'.freeze + VERSION = '3.2.1.rc.1'.freeze end diff --git a/lib/datastax/cassandra.rb b/lib/datastax/cassandra.rb index 89bd1f20c..1a306f97e 100644 --- a/lib/datastax/cassandra.rb +++ b/lib/datastax/cassandra.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/address_resolution/policies/ec2_multi_region_spec.rb b/spec/cassandra/address_resolution/policies/ec2_multi_region_spec.rb index 6e228b688..ca4e6c5ca 100644 --- a/spec/cassandra/address_resolution/policies/ec2_multi_region_spec.rb +++ b/spec/cassandra/address_resolution/policies/ec2_multi_region_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/auth/providers/password_spec.rb b/spec/cassandra/auth/providers/password_spec.rb index 41bdeb888..3eebe974b 100644 --- a/spec/cassandra/auth/providers/password_spec.rb +++ b/spec/cassandra/auth/providers/password_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ module Providers it 'returns nil when the authentication class is not org.apache.cassandra.auth.PasswordAuthenticator' do authenticator = auth_provider.create_authenticator('org.acme.Foo') - authenticator.should be_nil + authenticator.initial_response.should == "\x00foo\x00bar" end end end diff --git a/spec/cassandra/cluster/client_spec.rb b/spec/cassandra/cluster/client_spec.rb index 1823cd03f..97490fd67 100644 --- a/spec/cassandra/cluster/client_spec.rb +++ b/spec/cassandra/cluster/client_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,7 +40,8 @@ class Cluster } } let(:driver) { Driver.new(driver_settings) } - let(:client) { Client.new(driver.logger, driver.cluster_registry, driver.cluster_schema, driver.io_reactor, driver.connector, driver.load_balancing_policy, driver.reconnection_policy, driver.retry_policy, driver.address_resolution_policy, driver.connection_options, driver.futures_factory) } + let(:client) { Client.new(driver.logger, driver.cluster_registry, driver.cluster_schema, driver.io_reactor, driver.connector, driver.profile_manager, driver.reconnection_policy, driver.address_resolution_policy, driver.connection_options, driver.futures_factory, driver.timestamp_generator) } + let(:execution_options) { driver.execution_options } before do io_reactor.connection_options = driver.connection_options @@ -70,6 +71,12 @@ class Cluster expect(io_reactor).to have(4).connections end + it 'gets distance from profile-manager' do + driver.profile_manager.stub(:distance) { :remote } + client.connect.value + expect(io_reactor).to have(2).connections + end + it 'can be called multiple times' do future = client.connect expect(future).to eq(client.connect) @@ -263,6 +270,13 @@ class Cluster end end + it 'gets distance from policy_manager' do + driver.profile_manager.stub(:distance) { :remote } + expect do + client.host_up(Cassandra::Host.new('1.1.1.1')) + end.to change { io_reactor.connections.size }.from(4).to(5) + end + context 'host not responding' do let(:address) { '1.1.1.1' } let(:host) { Host.new(address) } @@ -375,7 +389,7 @@ class Cluster end end client.connect.value - client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options).get expect(handled).to be_truthy end @@ -403,10 +417,10 @@ class Cluster end end client.connect.value - client.query(Statements::Simple.new('USE foo'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('USE foo'), execution_options).get # make sure we get a different host in the load balancing plan cluster_registry.hosts.delete(cluster_registry.hosts.first) - client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options).get expect(count).to eq(2) end @@ -434,10 +448,10 @@ class Cluster end end client.connect.value - client.query(Statements::Simple.new('USE "FooBar"'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('USE "FooBar"'), execution_options).get # make sure we get a different host in the load balancing plan cluster_registry.hosts.delete(cluster_registry.hosts.first) - client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options).get expect(count).to eq(2) end @@ -458,7 +472,7 @@ class Cluster attempts << connection.host if count == 0 count += 1 - raise Cassandra::Errors::ClientError.new + raise Cassandra::Errors::InternalError.new else Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) end @@ -469,7 +483,73 @@ class Cluster end end client.connect.value - client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options).get + expect(attempts).to have(2).items + expect(attempts).to eq(hosts) + end + + it 'does not retry on client timeout error if statement is not idempotent' do + attempts = [] + io_reactor.on_connection do |connection| + connection.handle_request do |request| + case request + when Cassandra::Protocol::OptionsRequest + Cassandra::Protocol::SupportedResponse.new({}) + when Cassandra::Protocol::StartupRequest + Cassandra::Protocol::ReadyResponse.new + when Cassandra::Protocol::QueryRequest + case request.cql + when 'SELECT * FROM songs' + attempts << connection.host + raise Cassandra::Errors::TimeoutError.new + else + Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) + end + end + end + end + client.connect.value + future = client.query(Statements::Simple.new('SELECT * FROM songs'), + execution_options) + got_excp = false + future.on_failure do |ex| + got_excp = true + expect(ex).to be_a(Cassandra::Errors::TimeoutError) + end + expect(got_excp) + expect(attempts).to have(1).items + expect(attempts.first).to eq(hosts.first) + end + + it 'retries on client timeout error if statement is idempotent' do + count = 0 + attempts = [] + io_reactor.on_connection do |connection| + connection.handle_request do |request| + case request + when Cassandra::Protocol::OptionsRequest + Cassandra::Protocol::SupportedResponse.new({}) + when Cassandra::Protocol::StartupRequest + Cassandra::Protocol::ReadyResponse.new + when Cassandra::Protocol::QueryRequest + case request.cql + when 'SELECT * FROM songs' + attempts << connection.host + if count == 0 + count += 1 + raise raise Cassandra::Errors::TimeoutError.new + else + Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) + end + else + Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) + end + end + end + end + client.connect.value + client.query(Statements::Simple.new('SELECT * FROM songs', nil, nil, true), + execution_options).get expect(attempts).to have(2).items expect(attempts).to eq(hosts) end @@ -485,7 +565,7 @@ class Cluster when Cassandra::Protocol::QueryRequest case request.cql when 'SELECT * FROM songs' - raise Cassandra::Errors::ClientError.new + raise Cassandra::Errors::InternalError.new else Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) end @@ -494,7 +574,7 @@ class Cluster end client.connect.value expect do - client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options).get end.to raise_error(Errors::NoHostsAvailable) end @@ -519,7 +599,7 @@ class Cluster client.connect.value expect do - client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options).get end.to raise_error(Cassandra::Errors::InvalidError, 'blargh') end @@ -552,14 +632,14 @@ class Cluster end end client.connect.value - client.query(Statements::Simple.new('USE foo'), Execution::Options.new(:consistency => :one)).get + client.query(Statements::Simple.new('USE foo'), execution_options).get # make sure we get a different host in the load balancing plan cluster_registry.remove_host(cluster_registry.hosts.first) completed = 0 5.times do - f = client.query(Statements::Simple.new('SELECT * FROM songs'), Execution::Options.new(:consistency => :one)) + f = client.query(Statements::Simple.new('SELECT * FROM songs'), execution_options) f.on_success do completed += 1 end @@ -586,7 +666,7 @@ class Cluster end end client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get expect(statement.cql).to eq('SELECT * FROM songs') end end @@ -610,12 +690,12 @@ class Cluster end end client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get - client.execute(statement.bind, Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get + client.execute(statement.bind, execution_options).get expect(sent).to be_truthy end - it 're-prepares a statement on new connection' do + it 'does not re-prepare a statement on new connection if the new host already has the statement prepared' do count = 0 io_reactor.on_connection do |connection| connection.handle_request do |request| @@ -633,13 +713,18 @@ class Cluster end end client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get # make sure we get a different host in the load balancing plan cluster_registry.hosts.delete(cluster_registry.hosts.first) - client.execute(statement.bind, Execution::Options.new(:consistency => :one)).get - expect(count).to eq(2) + client.execute(statement.bind, execution_options).get + + # Expected sequence of events: + # 1. prepare on host1 + # 2. execute on host2. Since execute succeeds, the implication is that host2 already had the statement + # prepared. + expect(count).to eq(1) end it 're-prepares a statement on unprepared error' do @@ -666,13 +751,20 @@ class Cluster end end client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get # make sure we get a different host in the load balancing plan cluster_registry.hosts.delete(statement.execution_info.hosts.first) - client.execute(statement.bind, Execution::Options.new(:consistency => :one)).get - expect(count).to eq(3) + client.execute(statement.bind, execution_options).get + + + # Expected sequence of events: + # 1. prepare on host1 + # 2. execute on host2, yielding unprepared error. + # 3. prepare on host2 + # 4. execute on host2. + expect(count).to eq(2) expect(error).to be(false) end @@ -692,7 +784,7 @@ class Cluster attempts << connection.host if count == 0 count += 1 - raise Cassandra::Errors::ClientError.new + raise Cassandra::Errors::InternalError.new end Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) end @@ -700,9 +792,9 @@ class Cluster end client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get - client.execute(statement.bind, Execution::Options.new(:consistency => :one)).get + client.execute(statement.bind, execution_options).get expect(attempts).to have(2).items expect(attempts.sort!).to eq(hosts) @@ -726,13 +818,38 @@ class Cluster client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get expect do - client.execute(statement.bind, Execution::Options.new(:consistency => :one)).get + client.execute(statement.bind, execution_options).get end.to raise_error(Cassandra::Errors::InvalidError, 'blargh') end + it 'returns a Void result when there is a write-timeout with downgrading-consistency policy' do + driver_settings[:retry_policy] = Retry::Policies::DowngradingConsistency.new + io_reactor.on_connection do |connection| + connection.handle_request do |request| + case request + when Cassandra::Protocol::OptionsRequest + Cassandra::Protocol::SupportedResponse.new({}) + when Cassandra::Protocol::StartupRequest + Cassandra::Protocol::ReadyResponse.new + when Cassandra::Protocol::PrepareRequest + Protocol::PreparedResultResponse.new(nil, nil, 123, [], [], nil, nil) + when Cassandra::Protocol::ExecuteRequest + Protocol::WriteTimeoutErrorResponse.new(nil, nil, 123, 'write timeout', :one, 'x', 'y', 'simple') + end + end + end + + client.connect.value + + statement = client.prepare('SELECT * FROM songs', execution_options).get + + expect(client.execute(statement.bind, execution_options).get) + .to be_instance_of(Results::Void) + end + it 'raises if all hosts failed' do io_reactor.on_connection do |connection| connection.handle_request do |request| @@ -744,17 +861,17 @@ class Cluster when Cassandra::Protocol::PrepareRequest Protocol::PreparedResultResponse.new(nil, nil, 123, [], [], nil, nil) when Cassandra::Protocol::ExecuteRequest - raise Cassandra::Errors::ClientError.new + raise Cassandra::Errors::InternalError.new end end end client.connect.value - statement = client.prepare('SELECT * FROM songs', Execution::Options.new(:consistency => :one)).get + statement = client.prepare('SELECT * FROM songs', execution_options).get expect do - client.execute(statement.bind, Execution::Options.new(:consistency => :one)).get + client.execute(statement.bind, execution_options).get end.to raise_error(Errors::NoHostsAvailable) end end @@ -786,7 +903,7 @@ class Cluster allow(batch_request).to receive(:clear) expect(batch_request).to receive(:add_query).once.with('INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)', [1, 2, 3, 4, 5], [Cassandra::Types.bigint, Cassandra::Types.bigint, Cassandra::Types.bigint, Cassandra::Types.bigint, Cassandra::Types.bigint]) expect(batch_request).to receive(:retries=).once.with(0) - client.batch(batch, Execution::Options.new(:consistency => :one, :trace => false)).get + client.batch(batch, execution_options.override(consistency: :one)).get expect(sent).to be_truthy end @@ -819,7 +936,8 @@ class Cluster client.connect.value - statement = client.prepare('INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)', Execution::Options.new(:consistency => :one, :trace => false)).get + statement = client.prepare('INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)', + execution_options.override(consistency: :one)).get batch.add(statement, arguments: [Cassandra::Uuid.new(1), 'some title', 'some album', 'some artist', Set['cool', 'stuff']]) @@ -827,11 +945,11 @@ class Cluster allow(batch_request).to receive(:clear) expect(batch_request).to receive(:add_prepared).once.with(123, [Cassandra::Uuid.new(1), 'some title', 'some album', 'some artist', Set['cool', 'stuff']], params_metadata.map(&:last)) expect(batch_request).to receive(:retries=).once.with(0) - client.batch(batch, Execution::Options.new(:consistency => :one, :trace => false)).get + client.batch(batch, execution_options.override(consistency: :one)).get expect(sent).to be_truthy end - it 'automatically re-prepares statements' do + it 'does not automatically re-prepare statements' do sent = false count = 0 batch = Statements::Batch::Logged.new(driver.execution_options) @@ -862,7 +980,8 @@ class Cluster client.connect.value - statement = client.prepare('INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)', Execution::Options.new(:consistency => :one, :trace => false)).get + statement = client.prepare('INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)', + execution_options.override(consistency: :one)).get batch.add(statement, arguments: [Cassandra::Uuid.new(1), 'some title', 'some album', 'some artist', Set['cool', 'stuff']]) @@ -874,9 +993,13 @@ class Cluster # make sure we get a different host in the load balancing plan cluster_registry.hosts.delete(cluster_registry.hosts.first) - client.batch(batch, Execution::Options.new(:consistency => :one, :trace => false)).get + client.batch(batch, execution_options.override(consistency: :one)).get expect(sent).to be_truthy - expect(count).to eq(2) + + # Expected sequence of events: + # 1. prepare on host1 + # 2. run batch on host2 with prepared-statement id's. This succeeds, so no more work to do. + expect(count).to eq(1) end it 'follows the plan on failure' do @@ -893,7 +1016,7 @@ class Cluster attempts << connection.host if count == 0 count += 1 - raise Cassandra::Errors::ClientError.new + raise Cassandra::Errors::InternalError.new end Cassandra::Protocol::RowsResultResponse.new(nil, nil, [], [], nil, nil) end @@ -903,7 +1026,7 @@ class Cluster client.connect.value batch = Statements::Batch::Logged.new(driver.execution_options) - client.batch(batch, Execution::Options.new(:consistency => :one)).get + client.batch(batch, execution_options).get expect(attempts).to have(2).items expect(attempts).to eq(hosts) @@ -918,7 +1041,7 @@ class Cluster when Cassandra::Protocol::StartupRequest Cassandra::Protocol::ReadyResponse.new when Cassandra::Protocol::BatchRequest - raise Cassandra::Errors::ClientError.new + raise Cassandra::Errors::InternalError.new end end end @@ -928,7 +1051,7 @@ class Cluster batch = Statements::Batch::Logged.new(driver.execution_options) expect do - client.batch(batch, Execution::Options.new(:consistency => :one)).get + client.batch(batch, execution_options).get end.to raise_error(Errors::NoHostsAvailable) end end diff --git a/spec/cassandra/cluster/connection_pool_spec.rb b/spec/cassandra/cluster/connection_pool_spec.rb index 980d54a29..9db04aac3 100644 --- a/spec/cassandra/cluster/connection_pool_spec.rb +++ b/spec/cassandra/cluster/connection_pool_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster/control_connection_spec.rb b/spec/cassandra/cluster/control_connection_spec.rb index 0c1ffe662..b0b492356 100644 --- a/spec/cassandra/cluster/control_connection_spec.rb +++ b/spec/cassandra/cluster/control_connection_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ class Cluster end let :driver do - Driver.new(protocol_version: 7, io_reactor: io_reactor) + Driver.new(protocol_version: nil, io_reactor: io_reactor) end let :load_balancing_policy do @@ -63,6 +63,8 @@ class Cluster let(:schema_fetcher) { driver.schema_fetcher } + let(:local_ip) { ::IPAddr.new('127.0.0.1') } + def connections io_reactor.connections end @@ -114,6 +116,10 @@ def handle_request(&handler) ::Hash.new('rack1') end + let :tokens do + Hash.new(['token1']) + end + let :release_versions do ::Hash.new('2.0.7-SNAPSHOT') end @@ -139,6 +145,7 @@ def handle_request(&handler) end before do + driver.connection_options.protocol_version = 7 cluster_registry.add_listener(driver.load_balancing_policy) cluster_registry.add_listener(control_connection) cluster_registry.host_found('127.0.0.1') @@ -148,6 +155,7 @@ def handle_request(&handler) connection[:spec_data_center] = data_centers[connection.host] connection[:spec_host_id] = host_ids[connection.host] connection[:spec_release_version] = release_versions[connection.host] + connection[:spec_tokens] = tokens[connection.host] connection.handle_request do |request, timeout| additional_rpc_addresses = additional_nodes.dup @@ -167,17 +175,21 @@ def handle_request(&handler) 'rack' => connection[:spec_rack], 'data_center' => connection[:spec_data_center], 'host_id' => connection[:spec_host_id], - 'release_version' => connection[:spec_release_version] + 'release_version' => connection[:spec_release_version], + 'tokens' => connection[:spec_tokens] } Protocol::RowsResultResponse.new(nil, nil, [row], local_metadata, nil, nil) when /FROM system\.peers WHERE peer = '?(\S+)'/ ip = $1 rows = [ { + 'peer' => ip, 'rack' => racks[ip], 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], - 'release_version' => release_versions[ip] + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] } ] Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) @@ -190,7 +202,8 @@ def handle_request(&handler) 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, - 'release_version' => release_versions[ip] + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] } end Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) @@ -251,6 +264,35 @@ def handle_request(&handler) counter.should == 7 end + it 'gives up when the protocol version is non-negotiable' do + + driver = Driver.new(protocol_version: 3, io_reactor: io_reactor) + io_reactor.connection_options = driver.connection_options + control_conn = ControlConnection.new(driver.logger, + driver.io_reactor, + driver.cluster_registry, + driver.cluster_schema, + driver.cluster_metadata, + driver.load_balancing_policy, + driver.reconnection_policy, + driver.address_resolution_policy, + driver.connector, + driver.connection_options, + driver.schema_fetcher) + + driver.cluster_registry.add_listener(driver.load_balancing_policy) + driver.cluster_registry.add_listener(control_conn) + driver.cluster_registry.host_found('127.0.0.1') + + counter = 0 + handle_request do |request| + counter += 1 + Protocol::ErrorResponse.new(nil, nil, 0x0a, 'Bork version, dummy!') + end + expect { control_conn.connect_async.value }.to raise_error(Cassandra::Errors::ProtocolError, 'Bork version, dummy!') + expect(counter).to eq(1) + end + it 'gives up when a non-protocol version related error is raised' do handle_request do |request| Protocol::ErrorResponse.new(nil, nil, 0x1001, 'Get off my lawn!') @@ -288,6 +330,7 @@ def handle_request(&handler) host.rack.should == racks[ip] host.datacenter.should == data_centers[ip] host.release_version.should == release_versions[ip] + host.tokens.should == tokens[ip] end end @@ -295,12 +338,23 @@ def handle_request(&handler) it 'skips empty peers' do additional_rpc_addresses = additional_nodes.dup + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if 'peer' is empty. This is indicated by *not* doing + # an address resolution of rpc-address. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.1.2.3')) + handle_request do |request| case request when Protocol::QueryRequest case request.cql when /FROM system\.peers/ - rows = min_peers[0].times.map do |host_id| + rows = min_peers[0].times.map do ip = additional_rpc_addresses.shift { 'peer' => ip, @@ -308,17 +362,19 @@ def handle_request(&handler) 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, - 'release_version' => release_versions[ip] + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] } end rows << { - 'peer' => nil, - 'rack' => nil, - 'data_center' => nil, - 'host_id' => nil, - 'rpc_address' => nil, - 'release_version' => nil + 'peer' => nil, + 'rack' => racks['127.1.2.3'], + 'data_center' => data_centers['127.1.2.3'], + 'host_id' => host_ids['127.1.2.3'], + 'rpc_address' => '127.1.2.3', + 'release_version' => release_versions['127.1.2.3'], + 'tokens' => tokens['127.1.2.3'] } Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) end @@ -331,6 +387,376 @@ def handle_request(&handler) end end + context 'with empty rack' do + it 'skips empty rack' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if 'rack' is empty. This is indicated by *not* doing + # an address resolution of peer. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.1.2.3')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => '127.1.2.3', + 'rack' => nil, + 'data_center' => data_centers['127.1.2.3'], + 'host_id' => host_ids['127.1.2.3'], + 'rpc_address' => '127.1.2.3', + 'release_version' => release_versions['127.1.2.3'], + 'tokens' => tokens['127.1.2.3'] + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + end + + context 'with empty data_center' do + it 'skips empty data_center' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if 'datacenter' is empty. This is indicated by *not* doing + # an address resolution of peer. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.1.2.3')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => '127.1.2.3', + 'rack' => racks['127.1.2.3'], + 'data_center' => nil, + 'host_id' => host_ids['127.1.2.3'], + 'rpc_address' => '127.1.2.3', + 'release_version' => release_versions['127.1.2.3'], + 'tokens' => tokens['127.1.2.3'] + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + end + + context 'with empty host_id' do + it 'skips empty host_id' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if 'host_id' is empty. This is indicated by *not* doing + # an address resolution of peer. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.1.2.3')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => '127.1.2.3', + 'rack' => racks['127.1.2.3'], + 'data_center' => data_centers['127.1.2.3'], + 'host_id' => nil, + 'rpc_address' => '127.1.2.3', + 'release_version' => release_versions['127.1.2.3'], + 'tokens' => tokens['127.1.2.3'] + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + end + + context 'with empty rpc_address' do + it 'skips empty rpc_address' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if 'rpc-address' is empty. This is indicated by *not* doing + # an address resolution of peer. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.1.2.3')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => '127.1.2.3', + 'rack' => racks['127.1.2.3'], + 'data_center' => data_centers['127.1.2.3'], + 'host_id' => host_ids['127.1.2.3'], + 'rpc_address' => nil, + 'release_version' => release_versions['127.1.2.3'], + 'tokens' => tokens['127.1.2.3'] + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + end + + context 'with empty tokens' do + it 'skips empty tokens' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if 'rpc-address' is empty. This is indicated by *not* doing + # an address resolution of peer. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.1.2.3')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => '127.1.2.3', + 'rack' => racks['127.1.2.3'], + 'data_center' => data_centers['127.1.2.3'], + 'host_id' => host_ids['127.1.2.3'], + 'rpc_address' => '127.1.2.3', + 'release_version' => release_versions['127.1.2.3'], + 'tokens' => nil + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + end + + context 'with local node' do + it 'skips matching peer' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if its rpc_address is the local host. This is indicated by *not* doing + # an address resolution of it. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.0.0.9')) + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.0.0.1')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => connections.first.host, + 'rack' => racks['127.0.0.9'], + 'data_center' => data_centers['127.0.0.9'], + 'host_id' => host_ids['127.0.0.9'], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : '127.0.0.9', + 'release_version' => release_versions['127.0.0.9'], + 'tokens' => tokens['127.0.0.9'] + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + + it 'skips matching rpc_address' do + additional_rpc_addresses = additional_nodes.dup + + expect(address_resolution_policy).to receive(:resolve).with(local_ip).and_return(local_ip) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[0]).and_return(additional_rpc_addresses[0]) + expect(address_resolution_policy).to receive(:resolve).with(additional_rpc_addresses[1]).and_return(additional_rpc_addresses[1]) + + # RUBY-255: We should never try to do address resolution of nil. + expect(address_resolution_policy).to_not receive(:resolve).with(nil) + + # We should give up on a peer if its rpc_address is the local host. This is indicated by *not* doing + # an address resolution of it. + expect(address_resolution_policy).to_not receive(:resolve).with(::IPAddr.new('127.0.0.9')) + + handle_request do |request| + case request + when Protocol::QueryRequest + case request.cql + when /FROM system\.peers/ + rows = min_peers[0].times.map do + ip = additional_rpc_addresses.shift + { + 'peer' => ip, + 'rack' => racks[ip], + 'data_center' => data_centers[ip], + 'host_id' => host_ids[ip], + 'rpc_address' => bind_all_rpc_addresses ? ::IPAddr.new('0.0.0.0') : ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] + } + end + + rows << { + 'peer' => '127.0.0.9', + 'rack' => racks['127.0.0.9'], + 'data_center' => data_centers['127.0.0.9'], + 'host_id' => host_ids['127.0.0.9'], + 'rpc_address' => connections.first.host, + 'release_version' => release_versions['127.0.0.9'], + 'tokens' => tokens['127.0.0.9'] + } + Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) + end + end + end + + control_connection.connect_async.value + + expect(cluster_registry).to have(3).hosts + end + end + context 'when the nodes have 0.0.0.0 as rpc_address' do let :bind_all_rpc_addresses do true @@ -345,6 +771,7 @@ def handle_request(&handler) host.rack.should == racks[ip] host.datacenter.should == data_centers[ip] host.release_version.should == release_versions[ip] + host.tokens.should == tokens[ip] end end end @@ -451,11 +878,15 @@ def handle_request(&handler) it 'refreshes metadata and notifies registry' do ip = address.to_s + expect(address_resolution_policy).to receive(:resolve).with(address).and_return(address).twice expect(cluster_registry).to receive(:host_found).once.with(address, { + 'peer' => address, 'rack' => racks[ip], 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], - 'release_version' => release_versions[ip] + 'rpc_address' => address, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] }) connections.first.trigger_event(event) @@ -478,9 +909,8 @@ def handle_request(&handler) logger.should have_received(:debug).with(/Event received EVENT STATUS_CHANGE DOWN/) end - it 'notifies registry' do - ip = address.to_s - expect(cluster_registry).to receive(:host_down).once.with(ip) + it 'does not notify registry' do + expect(cluster_registry).to_not receive(:host_down) connections.first.trigger_event(event) end end @@ -517,11 +947,15 @@ def handle_request(&handler) it 'notifies registry' do ip = address.to_s + expect(address_resolution_policy).to receive(:resolve).with(address).and_return(address).twice expect(cluster_registry).to receive(:host_found).once.with(address, { + 'peer' => address, 'rack' => racks[ip], 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], - 'release_version' => release_versions[ip] + 'rpc_address' => address, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] }) connections.first.trigger_event(event) end @@ -559,10 +993,13 @@ def handle_request(&handler) ip = $1 rows = [ { + 'peer' => ip, 'rack' => racks[ip], 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], - 'release_version' => release_versions[ip] + 'rpc_address' => ip, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] } ] Protocol::RowsResultResponse.new(nil, nil, rows, peer_metadata, nil, nil) @@ -581,11 +1018,15 @@ def handle_request(&handler) it 'tries again' do connections.first.trigger_event(event) ip = address.to_s + expect(address_resolution_policy).to receive(:resolve).with(address).and_return(address) expect(cluster_registry).to receive(:host_found).once.with(address, { + 'peer' => address, 'rack' => racks[ip], 'data_center' => data_centers[ip], 'host_id' => host_ids[ip], - 'release_version' => release_versions[ip] + 'rpc_address' => address, + 'release_version' => release_versions[ip], + 'tokens' => tokens[ip] }) io_reactor.advance_time(reconnect_interval) end diff --git a/spec/cassandra/cluster/metadata_spec.rb b/spec/cassandra/cluster/metadata_spec.rb index 7db26b904..eeb10b767 100644 --- a/spec/cassandra/cluster/metadata_spec.rb +++ b/spec/cassandra/cluster/metadata_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ class FakeSchema < Cassandra::Cluster::Schema def add_keyspace(name, replication_class, replication_options) replication = Cassandra::Keyspace::Replication.new(replication_class, replication_options) - @keyspaces[name] = Cassandra::Keyspace.new(name, true, replication, {}, {}, {}, {}) + @keyspaces[name] = Cassandra::Keyspace.new(name, true, replication, {}, {}, {}, {}, {}) self end end diff --git a/spec/cassandra/cluster/options_spec.rb b/spec/cassandra/cluster/options_spec.rb index cc1f6a5cc..05c4a3bec 100644 --- a/spec/cassandra/cluster/options_spec.rb +++ b/spec/cassandra/cluster/options_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,46 +22,57 @@ def make_options(logger, protocol_version, connections_per_local_node, connections_per_remote_node, - requests_per_connection) + requests_per_connection, + allow_boolean_protocol) Cassandra::Cluster::Options.new( logger, protocol_version, nil, nil, nil, nil, nil, false, connections_per_local_node, connections_per_remote_node, 60, 30, true, 1, 10, - true, true, requests_per_connection) + true, requests_per_connection, [], allow_boolean_protocol) end module Cassandra class Cluster describe(Options) do let(:logger) { Cassandra::NullLogger.new } + it 'should set the protocol-version to max-supported if not in beta' do + expect(make_options(logger, nil, nil, nil, nil, false). + protocol_version).to eq(Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION) + end + + it 'should set the protocol-version to beta version if allow-beta is true' do + expect(make_options(logger, nil, nil, nil, nil, true). + protocol_version).to eq(Cassandra::Protocol::Versions::BETA_VERSION) + end + context :connections_per_local_node do it 'should return the default value for v2' do - expect(make_options(logger, 2, nil, nil, nil). + expect(make_options(logger, 2, nil, nil, nil, false). connections_per_local_node).to eq(2) end it 'should return the default value for v3' do - expect(make_options(logger, 3, nil, nil, nil). + expect(make_options(logger, 3, nil, nil, nil, false). connections_per_local_node).to eq(1) end it 'should return the user-specified value' do - expect(make_options(logger, 3, 12, nil, nil). + expect(make_options(logger, 3, 12, nil, nil, false). connections_per_local_node).to eq(12) - expect(make_options(logger, 2, 13, nil, nil). + expect(make_options(logger, 2, 13, nil, nil, false). connections_per_local_node).to eq(13) end end context :connections_per_remote_node do it 'should return the default value' do - expect(make_options(logger, 2, nil, nil, nil). + expect(make_options(logger, 2, nil, nil, nil, false). connections_per_remote_node).to eq(1) - expect(make_options(logger, 3, nil, nil, nil). + expect(make_options(logger, 3, nil, nil, nil, false). connections_per_remote_node).to eq(1) end it 'should return the user-specified value' do - expect(make_options(logger, 3, nil, 14, nil). + expect(make_options(logger, 3, nil, 14, nil, false). connections_per_remote_node).to eq(14) end end @@ -71,31 +82,31 @@ class Cluster it 'should return the default value for v2' do expect(logger).to_not receive(:warn) - expect(make_options(logger, 2, nil, nil, nil). + expect(make_options(logger, 2, nil, nil, nil, false). requests_per_connection).to eq(128) end it 'should return the default value for v3' do expect(logger).to_not receive(:warn) - expect(make_options(logger, 3, nil, nil, nil). + expect(make_options(logger, 3, nil, nil, nil, false). requests_per_connection).to eq(1024) end it 'should return the user-specified value' do expect(logger).to_not receive(:warn) - expect(make_options(logger, 2, nil, nil, 13). + expect(make_options(logger, 2, nil, nil, 13, false). requests_per_connection).to eq(13) - expect(make_options(logger, 3, nil, nil, 14). + expect(make_options(logger, 3, nil, nil, 14, false). requests_per_connection).to eq(14) end it 'should pull down the value to 128 if requested value is too high for v2' do expect(logger).to receive(:warn) - expect(make_options(logger, 2, nil, nil, 150). + expect(make_options(logger, 2, nil, nil, 150, false). requests_per_connection).to eq(128) end it 'should not adjust high value for v3' do expect(logger).to_not receive(:warn) - expect(make_options(logger, 3, nil, nil, 150). + expect(make_options(logger, 3, nil, nil, 150, false). requests_per_connection).to eq(150) end end diff --git a/spec/cassandra/cluster/registry_spec.rb b/spec/cassandra/cluster/registry_spec.rb index 3599fc995..976351d44 100644 --- a/spec/cassandra/cluster/registry_spec.rb +++ b/spec/cassandra/cluster/registry_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,19 +34,120 @@ class Cluster context('when host is unknown') do it 'notifies listeners' do + expect(listener).to receive(:host_found).twice do |host| + expect(host.tokens).to eq([]) + expect(host).to be_up + + if host.ip == ::IPAddr.new('127.0.0.1') + expect(host.broadcast_address).to be_nil + expect(host.listen_address).to be_nil + else + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.127')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.128')) + end + end + + expect(listener).to receive(:host_up).twice do |host| + expect(host.tokens).to eq([]) + expect(host).to be_up + + if host.ip == ::IPAddr.new('127.0.0.1') + expect(host.broadcast_address).to be_nil + expect(host.listen_address).to be_nil + else + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.127')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.128')) + end + end + + subject.host_found(::IPAddr.new('127.0.0.1'), {}) + subject.host_found(::IPAddr.new('127.0.0.2'), { + 'broadcast_address' => '127.0.0.127', + 'listen_address' => '127.0.0.128' + }) + end + end + + context('when host is known') do + before do + allow(listener).to receive(:host_found) + allow(listener).to receive(:host_up) + subject.host_found(::IPAddr.new('127.0.0.2'), { + 'broadcast_address' => '127.0.0.127', + 'listen_address' => '127.0.0.128' + }) + end + let (:host) { + subject.host('127.0.0.2') + } + + it 'notifies listeners when doing down to up' do + # First bring down the host; we want to test that bringing it up doesn't lose + # info. + + host.instance_variable_set(:@status, :down) + + # Now bring it up and verify that it still has broadcast_address and + # listen_address. + expect(listener).to receive(:host_up).once do |host| + expect(host.tokens).to eq([]) + expect(host).to be_up + + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.127')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.128')) + end + + subject.host_found(::IPAddr.new('127.0.0.2'), { + 'broadcast_address' => '127.0.0.127', + 'listen_address' => '127.0.0.128' + }) + end + + it 'notifies listeners when doing up to down' do + expect(listener).to receive(:host_down).once do |host| + expect(host.tokens).to eq([]) + expect(host).to be_down + + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.127')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.128')) + end + + subject.host_down(::IPAddr.new('127.0.0.2')) + end + + it 're-creates host when metadata has changed' do + expect(listener).to receive(:host_down).once do |host| + expect(host.tokens).to eq([]) + expect(host).to be_down + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.127')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.128')) + end + + expect(listener).to receive(:host_lost).once do |host| + expect(host.tokens).to eq([]) + expect(host).to be_down + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.127')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.128')) + end + expect(listener).to receive(:host_found).once do |host| - expect(host.ip).to eq(::IPAddr.new('127.0.0.1')) expect(host.tokens).to eq([]) expect(host).to be_up + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.227')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.228')) end expect(listener).to receive(:host_up).once do |host| - expect(host.ip).to eq(::IPAddr.new('127.0.0.1')) expect(host.tokens).to eq([]) expect(host).to be_up + expect(host.broadcast_address).to eq(::IPAddr.new('127.0.0.227')) + expect(host.listen_address).to eq(::IPAddr.new('127.0.0.228')) end - subject.host_found(::IPAddr.new('127.0.0.1'), {}) + subject.host_found(::IPAddr.new('127.0.0.2'), { + 'broadcast_address' => '127.0.0.227', + 'listen_address' => '127.0.0.228' + }) end end end diff --git a/spec/cassandra/cluster/schema/fetchers/1.2.19-data.json b/spec/cassandra/cluster/schema/fetchers/1.2.19-data.json index f8ddcd849..fa742db2b 100644 --- a/spec/cassandra/cluster/schema/fetchers/1.2.19-data.json +++ b/spec/cassandra/cluster/schema/fetchers/1.2.19-data.json @@ -11,6 +11,12 @@ "durable_writes": true, "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", "strategy_options": "{\"replication_factor\":\"2\"}" + }, + { + "keyspace_name": "simplex", + "durable_writes": true, + "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", + "strategy_options": "{\"replication_factor\":\"1\"}" } ], "SELECT * FROM system.schema_columnfamilies": [ @@ -472,6 +478,87 @@ "subcomparator": null, "type": "Standard", "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": "KEYS_ONLY", + "column_aliases": "[\"f2\"]", + "comment": "test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "gc_grace_seconds": 2, + "id": null, + "key_alias": null, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "min_compaction_threshold": 4, + "populate_io_cache_on_flush": false, + "read_repair_chance": 0.1, + "replicate_on_write": true, + "subcomparator": null, + "type": "Standard", + "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "bloom_filter_fp_chance": 0.01, + "caching": "KEYS_ONLY", + "column_aliases": "[\"f2\"]", + "comment": "compact test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_validator": "org.apache.cassandra.db.marshal.Int32Type", + "gc_grace_seconds": 2, + "id": null, + "key_alias": null, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "min_compaction_threshold": 4, + "populate_io_cache_on_flush": false, + "read_repair_chance": 0.1, + "replicate_on_write": true, + "subcomparator": null, + "type": "Standard", + "value_alias": "f3" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "bloom_filter_fp_chance": 0.01, + "caching": "KEYS_ONLY", + "column_aliases": "[]", + "comment": "test table one column key", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "gc_grace_seconds": 2, + "id": null, + "key_alias": null, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "min_compaction_threshold": 4, + "populate_io_cache_on_flush": false, + "read_repair_chance": 0.1, + "replicate_on_write": true, + "subcomparator": null, + "type": "Standard", + "value_alias": null } ], "SELECT * FROM system.schema_columns": [ @@ -1114,6 +1201,26 @@ "index_options": null, "index_type": null, "validator": "org.apache.cassandra.db.marshal.DateType" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f3", + "component_index": 1, + "index_name": "ind1", + "index_options": "{\"prefix_size\":\"1\"}", + "index_type": "COMPOSITES", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "validator": "org.apache.cassandra.db.marshal.Int32Type" } ] } \ No newline at end of file diff --git a/spec/cassandra/cluster/schema/fetchers/1.2.19-schema.cql b/spec/cassandra/cluster/schema/fetchers/1.2.19-schema.cql index c5e66c324..e99cb6d61 100644 --- a/spec/cassandra/cluster/schema/fetchers/1.2.19-schema.cql +++ b/spec/cassandra/cluster/schema/fetchers/1.2.19-schema.cql @@ -1,10 +1,10 @@ CREATE KEYSPACE system WITH replication = {'class': 'LocalStrategy'} AND durable_writes = true; CREATE TABLE system."HintsColumnFamily" ( - key blob, - column1 blob, + "key" blob, + "column1" blob, value blob, - PRIMARY KEY (key, column1) + PRIMARY KEY ("key", "column1") ) WITH COMPACT STORAGE AND bloom_filter_fp_chance = 0.01 @@ -36,10 +36,10 @@ WITH COMPACT STORAGE AND replicate_on_write = 'true'; CREATE TABLE system."LocationInfo" ( - key blob, - column1 blob, + "key" blob, + "column1" blob, value blob, - PRIMARY KEY (key, column1) + PRIMARY KEY ("key", "column1") ) WITH COMPACT STORAGE AND bloom_filter_fp_chance = 0.01 @@ -54,10 +54,10 @@ WITH COMPACT STORAGE AND replicate_on_write = 'true'; CREATE TABLE system."Migrations" ( - key blob, - column1 timeuuid, + "key" blob, + "column1" timeuuid, value blob, - PRIMARY KEY (key, column1) + PRIMARY KEY ("key", "column1") ) WITH COMPACT STORAGE AND bloom_filter_fp_chance = 0.01 @@ -72,9 +72,9 @@ WITH COMPACT STORAGE AND replicate_on_write = 'true'; CREATE TABLE system."NodeIdInfo" ( - key text, + "key" text, id timeuuid, - PRIMARY KEY (key, id) + PRIMARY KEY ("key", id) ) WITH COMPACT STORAGE AND bloom_filter_fp_chance = 0.01 @@ -89,10 +89,10 @@ WITH COMPACT STORAGE AND replicate_on_write = 'true'; CREATE TABLE system."Schema" ( - key blob, - column1 text, + "key" blob, + "column1" text, value blob, - PRIMARY KEY (key, column1) + PRIMARY KEY ("key", "column1") ) WITH COMPACT STORAGE AND bloom_filter_fp_chance = 0.01 @@ -142,7 +142,7 @@ WITH COMPACT STORAGE AND replicate_on_write = 'true'; CREATE TABLE system.local ( - key text PRIMARY KEY, + "key" text PRIMARY KEY, bootstrapped text, cluster_name text, cql_version text, @@ -243,7 +243,7 @@ CREATE TABLE system.schema_columnfamilies ( read_repair_chance double, replicate_on_write boolean, subcomparator text, - type text, + "type" text, value_alias text, PRIMARY KEY (keyspace_name, columnfamily_name) ) @@ -337,4 +337,59 @@ WITH bloom_filter_fp_chance = 0.01 AND gc_grace_seconds = 0 AND populate_io_cache_on_flush = 'false' AND read_repair_chance = 0.0 - AND replicate_on_write = 'true'; \ No newline at end of file + AND replicate_on_write = 'true'; + +CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE simplex."t1" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = 'KEYS_ONLY' + AND comment = 'test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND gc_grace_seconds = 2 + AND populate_io_cache_on_flush = 'false' + AND read_repair_chance = 0.1 + AND replicate_on_write = 'true'; + +CREATE INDEX "ind1" ON simplex."t1" ("f3"); + +CREATE TABLE simplex."t2" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH CLUSTERING ORDER BY ("f2" DESC) + AND COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = 'KEYS_ONLY' + AND comment = 'compact test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND gc_grace_seconds = 2 + AND populate_io_cache_on_flush = 'false' + AND read_repair_chance = 0.1 + AND replicate_on_write = 'true'; + +CREATE TABLE simplex."t3" ( + "f1" int PRIMARY KEY, + "f2" int +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = 'KEYS_ONLY' + AND comment = 'test table one column key' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND gc_grace_seconds = 2 + AND populate_io_cache_on_flush = 'false' + AND read_repair_chance = 0.1 + AND replicate_on_write = 'true'; diff --git a/spec/cassandra/cluster/schema/fetchers/2.0.16-data.json b/spec/cassandra/cluster/schema/fetchers/2.0.16-data.json index 201e99b69..2e752af6d 100644 --- a/spec/cassandra/cluster/schema/fetchers/2.0.16-data.json +++ b/spec/cassandra/cluster/schema/fetchers/2.0.16-data.json @@ -11,6 +11,12 @@ "durable_writes": true, "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", "strategy_options": "{\"replication_factor\":\"2\"}" + }, + { + "keyspace_name": "simplex", + "durable_writes": true, + "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", + "strategy_options": "{\"replication_factor\":\"1\"}" } ], "SELECT * FROM system.schema_columnfamilies": [ @@ -571,6 +577,99 @@ "subcomparator": null, "type": "Standard", "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": "KEYS_ONLY", + "column_aliases": "[f2]", + "comment": "test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "index_interval": 128, + "is_dense": false, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "populate_io_cache_on_flush": false, + "read_repair_chance": 0.1, + "replicate_on_write": true, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard", + "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "bloom_filter_fp_chance": 0.01, + "caching": "KEYS_ONLY", + "column_aliases": "[f2]", + "comment": "compact test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.Int32Type", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "index_interval": 128, + "is_dense": false, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "populate_io_cache_on_flush": false, + "read_repair_chance": 0.1, + "replicate_on_write": true, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard", + "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "bloom_filter_fp_chance": 0.01, + "caching": "KEYS_ONLY", + "column_aliases": "[]", + "comment": "test table one column key", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "index_interval": 128, + "is_dense": false, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "populate_io_cache_on_flush": false, + "read_repair_chance": 0.1, + "replicate_on_write": true, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard", + "value_alias": null } ], "SELECT * FROM system.schema_columns": [ @@ -1948,6 +2047,102 @@ "index_type": null, "type": "regular", "validator": "org.apache.cassandra.db.marshal.TimestampType" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f2", + "component_index": 0, + "index_name": "ind1", + "index_options": "{}", + "index_type": "COMPOSITES", + "type": "clustering_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f3", + "component_index": 1, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "clustering_key", + "validator": "org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f3", + "component_index": 1, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + } + ], + "SELECT * FROM system.schema_triggers": [ + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "trigger_name": "mytrigger", + "trigger_options": { "class": "org.apache.cassandra.triggers.AuditTrigger"} } ] -} \ No newline at end of file +} diff --git a/spec/cassandra/cluster/schema/fetchers/2.0.16-schema.cql b/spec/cassandra/cluster/schema/fetchers/2.0.16-schema.cql index 71ef02c6a..3bb79c272 100644 --- a/spec/cassandra/cluster/schema/fetchers/2.0.16-schema.cql +++ b/spec/cassandra/cluster/schema/fetchers/2.0.16-schema.cql @@ -22,9 +22,9 @@ WITH COMPACT STORAGE AND speculative_retry = '99.0PERCENTILE'; CREATE TABLE system."NodeIdInfo" ( - key text, + "key" text, id timeuuid, - PRIMARY KEY (key, id) + PRIMARY KEY ("key", id) ) WITH COMPACT STORAGE AND bloom_filter_fp_chance = 0.01 @@ -132,7 +132,7 @@ WITH COMPACT STORAGE AND speculative_retry = '99.0PERCENTILE'; CREATE TABLE system.local ( - key text PRIMARY KEY, + "key" text PRIMARY KEY, bootstrapped text, broadcast_address inet, cluster_name text, @@ -282,7 +282,7 @@ CREATE TABLE system.schema_columnfamilies ( replicate_on_write boolean, speculative_retry text, subcomparator text, - type text, + "type" text, value_alias text, PRIMARY KEY (keyspace_name, columnfamily_name) ) @@ -309,7 +309,7 @@ CREATE TABLE system.schema_columns ( index_name text, index_options text, index_type text, - type text, + "type" text, validator text, PRIMARY KEY (keyspace_name, columnfamily_name, column_name) ) @@ -376,8 +376,8 @@ CREATE TABLE system.sstable_activity ( keyspace_name text, columnfamily_name text, generation int, - rate_120m double, - rate_15m double, + "rate_120m" double, + "rate_15m" double, PRIMARY KEY ((keyspace_name, columnfamily_name, generation)) ) WITH bloom_filter_fp_chance = 0.01 @@ -442,4 +442,73 @@ WITH bloom_filter_fp_chance = 0.01 AND populate_io_cache_on_flush = 'false' AND read_repair_chance = 0.0 AND replicate_on_write = 'true' - AND speculative_retry = '99.0PERCENTILE'; \ No newline at end of file + AND speculative_retry = '99.0PERCENTILE'; + +CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE simplex."t1" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = 'KEYS_ONLY' + AND comment = 'test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND index_interval = 128 + AND memtable_flush_period_in_ms = 3600000 + AND populate_io_cache_on_flush = 'false' + AND read_repair_chance = 0.1 + AND replicate_on_write = 'true' + AND speculative_retry = '99.0PERCENTILE'; + +CREATE INDEX "ind1" ON simplex."t1" ("f2"); + +CREATE TRIGGER mytrigger ON simplex."t1" USING 'org.apache.cassandra.triggers.AuditTrigger'; + +CREATE TABLE simplex."t2" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH CLUSTERING ORDER BY ("f2" DESC) + AND COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = 'KEYS_ONLY' + AND comment = 'compact test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND index_interval = 128 + AND memtable_flush_period_in_ms = 3600000 + AND populate_io_cache_on_flush = 'false' + AND read_repair_chance = 0.1 + AND replicate_on_write = 'true' + AND speculative_retry = '99.0PERCENTILE'; + +CREATE TABLE simplex."t3" ( + "f1" int PRIMARY KEY, + "f2" int +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = 'KEYS_ONLY' + AND comment = 'test table one column key' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND index_interval = 128 + AND memtable_flush_period_in_ms = 3600000 + AND populate_io_cache_on_flush = 'false' + AND read_repair_chance = 0.1 + AND replicate_on_write = 'true' + AND speculative_retry = '99.0PERCENTILE'; diff --git a/spec/cassandra/cluster/schema/fetchers/2.1.9-data.json b/spec/cassandra/cluster/schema/fetchers/2.1.9-data.json index cc279f48f..ec0b197d7 100644 --- a/spec/cassandra/cluster/schema/fetchers/2.1.9-data.json +++ b/spec/cassandra/cluster/schema/fetchers/2.1.9-data.json @@ -11,6 +11,12 @@ "durable_writes": true, "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", "strategy_options": "{\"replication_factor\":\"2\"}" + }, + { + "keyspace_name": "simplex", + "durable_writes": true, + "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", + "strategy_options": "{\"replication_factor\":\"1\"}" } ], "SELECT * FROM system.schema_columnfamilies": [ @@ -621,6 +627,96 @@ "subcomparator": null, "type": "Standard", "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": "{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}", + "cf_id": "c5e99f16-8677-3914-b17e-960613512345", + "comment": "test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "is_dense": false, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "min_index_interval": 128, + "read_repair_chance": 0.1, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard", + "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "bloom_filter_fp_chance": 0.01, + "caching": "{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}", + "cf_id": "c5e99f16-8677-3914-b17e-960613512345", + "comment": "compact test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.Int32Type", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "is_dense": false, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "min_index_interval": 128, + "read_repair_chance": 0.1, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard", + "value_alias": null + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "bloom_filter_fp_chance": 0.01, + "caching": "{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}", + "cf_id": "c5e99f16-8677-3914-b17e-960613512345", + "comment": "test table one column key", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "is_dense": false, + "key_aliases": "[\"f1\"]", + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "min_index_interval": 128, + "read_repair_chance": 0.1, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard", + "value_alias": null } ], "SELECT * FROM system.schema_columns": [ @@ -2097,9 +2193,105 @@ "index_type": null, "type": "regular", "validator": "org.apache.cassandra.db.marshal.TimestampType" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f2", + "component_index": 0, + "index_name": "ind1", + "index_options": "{}", + "index_type": "COMPOSITES", + "type": "clustering_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f3", + "component_index": 1, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "clustering_key", + "validator": "org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f3", + "component_index": 1, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" } ], "SELECT * FROM system.schema_usertypes": [ + ], + "SELECT * FROM system.schema_triggers": [ + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "trigger_name": "mytrigger", + "trigger_options": { "class": "org.apache.cassandra.triggers.AuditTrigger"} + } ] -} \ No newline at end of file +} diff --git a/spec/cassandra/cluster/schema/fetchers/2.1.9-schema.cql b/spec/cassandra/cluster/schema/fetchers/2.1.9-schema.cql index 970bf5af3..de8e0f771 100644 --- a/spec/cassandra/cluster/schema/fetchers/2.1.9-schema.cql +++ b/spec/cassandra/cluster/schema/fetchers/2.1.9-schema.cql @@ -106,7 +106,7 @@ WITH COMPACT STORAGE AND speculative_retry = '99.0PERCENTILE'; CREATE TABLE system.local ( - key text PRIMARY KEY, + "key" text PRIMARY KEY, bootstrapped text, broadcast_address inet, cluster_name text, @@ -253,7 +253,7 @@ CREATE TABLE system.schema_columnfamilies ( read_repair_chance double, speculative_retry text, subcomparator text, - type text, + "type" text, value_alias text, PRIMARY KEY (keyspace_name, columnfamily_name) ) @@ -279,7 +279,7 @@ CREATE TABLE system.schema_columns ( index_name text, index_options text, index_type text, - type text, + "type" text, validator text, PRIMARY KEY (keyspace_name, columnfamily_name, column_name) ) @@ -387,8 +387,8 @@ CREATE TABLE system.sstable_activity ( keyspace_name text, columnfamily_name text, generation int, - rate_120m double, - rate_15m double, + "rate_120m" double, + "rate_15m" double, PRIMARY KEY ((keyspace_name, columnfamily_name, generation)) ) WITH bloom_filter_fp_chance = 0.01 @@ -450,4 +450,70 @@ WITH bloom_filter_fp_chance = 0.01 AND memtable_flush_period_in_ms = 3600000 AND min_index_interval = 128 AND read_repair_chance = 0.0 - AND speculative_retry = '99.0PERCENTILE'; \ No newline at end of file + AND speculative_retry = '99.0PERCENTILE'; + +CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE simplex."t1" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' + AND comment = 'test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 3600000 + AND min_index_interval = 128 + AND read_repair_chance = 0.1 + AND speculative_retry = '99.0PERCENTILE'; + +CREATE INDEX "ind1" ON simplex."t1" ("f2"); + +CREATE TRIGGER mytrigger ON simplex."t1" USING 'org.apache.cassandra.triggers.AuditTrigger'; + +CREATE TABLE simplex."t2" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH CLUSTERING ORDER BY ("f2" DESC) + AND COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' + AND comment = 'compact test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 3600000 + AND min_index_interval = 128 + AND read_repair_chance = 0.1 + AND speculative_retry = '99.0PERCENTILE'; + +CREATE TABLE simplex."t3" ( + "f1" int PRIMARY KEY, + "f2" int +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' + AND comment = 'test table one column key' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 3600000 + AND min_index_interval = 128 + AND read_repair_chance = 0.1 + AND speculative_retry = '99.0PERCENTILE'; diff --git a/spec/cassandra/cluster/schema/fetchers/2.2.1-data.json b/spec/cassandra/cluster/schema/fetchers/2.2.1-data.json index 5111eb146..cde2cb06b 100644 --- a/spec/cassandra/cluster/schema/fetchers/2.2.1-data.json +++ b/spec/cassandra/cluster/schema/fetchers/2.2.1-data.json @@ -23,6 +23,12 @@ "durable_writes": true, "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", "strategy_options": "{\"replication_factor\":\"2\"}" + }, + { + "keyspace_name": "simplex", + "durable_writes": true, + "strategy_class": "org.apache.cassandra.locator.SimpleStrategy", + "strategy_options": "{\"replication_factor\":\"1\"}" } ], "SELECT * FROM system.schema_columnfamilies": [ @@ -809,6 +815,90 @@ "speculative_retry": "99.0PERCENTILE", "subcomparator": null, "type": "Standard" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": "{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}", + "cf_id": "c5e99f16-8677-3914-b17e-960613512345", + "comment": "test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "is_dense": false, + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "min_index_interval": 128, + "read_repair_chance": 0.1, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "bloom_filter_fp_chance": 0.01, + "caching": "{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}", + "cf_id": "c5e99f16-8677-3914-b17e-960613512345", + "comment": "compact test table", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.Int32Type", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "is_dense": false, + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "min_index_interval": 128, + "read_repair_chance": 0.1, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "bloom_filter_fp_chance": 0.01, + "caching": "{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}", + "cf_id": "c5e99f16-8677-3914-b17e-960613512345", + "comment": "test table one column key", + "compaction_strategy_class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "compaction_strategy_options": "{}", + "comparator": "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UTF8Type)", + "compression_parameters": "{\"sstable_compression\":\"org.apache.cassandra.io.compress.LZ4Compressor\"}", + "default_time_to_live": 1, + "default_validator": "org.apache.cassandra.db.marshal.BytesType", + "dropped_columns": null, + "gc_grace_seconds": 2, + "is_dense": false, + "key_validator": "org.apache.cassandra.db.marshal.Int32Type", + "local_read_repair_chance": 0.1, + "max_compaction_threshold": 32, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 3600000, + "min_compaction_threshold": 4, + "min_index_interval": 128, + "read_repair_chance": 0.1, + "speculative_retry": "99.0PERCENTILE", + "subcomparator": null, + "type": "Standard" } ], "SELECT * FROM system.schema_columns": [ @@ -2857,6 +2947,94 @@ "index_type": null, "type": "regular", "validator": "org.apache.cassandra.db.marshal.TimestampType" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f2", + "component_index": 0, + "index_name": "ind1", + "index_options": "{}", + "index_type": "COMPOSITES", + "type": "clustering_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "column_name": "f3", + "component_index": 1, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "clustering_key", + "validator": "org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t2", + "column_name": "f3", + "component_index": 1, + "index_name": null, + "index_options": null, + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f1", + "component_index": null, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "partition_key", + "validator": "org.apache.cassandra.db.marshal.Int32Type" + }, + { + "keyspace_name": "simplex", + "columnfamily_name": "t3", + "column_name": "f2", + "component_index": 0, + "index_name": null, + "index_options": "null", + "index_type": null, + "type": "regular", + "validator": "org.apache.cassandra.db.marshal.Int32Type" } ], "SELECT * FROM system.schema_usertypes": [ @@ -2867,5 +3045,13 @@ ], "SELECT * FROM system.schema_aggregates": [ + ], + "SELECT * FROM system.schema_triggers": [ + { + "keyspace_name": "simplex", + "columnfamily_name": "t1", + "trigger_name": "mytrigger", + "trigger_options": { "class": "org.apache.cassandra.triggers.AuditTrigger"} + } ] -} \ No newline at end of file +} diff --git a/spec/cassandra/cluster/schema/fetchers/2.2.1-schema.cql b/spec/cassandra/cluster/schema/fetchers/2.2.1-schema.cql index 496331311..f5709d551 100644 --- a/spec/cassandra/cluster/schema/fetchers/2.2.1-schema.cql +++ b/spec/cassandra/cluster/schema/fetchers/2.2.1-schema.cql @@ -2,8 +2,8 @@ CREATE KEYSPACE system_auth WITH replication = {'class': 'SimpleStrategy', 'repl CREATE TABLE system_auth.resource_role_permissons_index ( resource text, - role text, - PRIMARY KEY (resource, role) + "role" text, + PRIMARY KEY (resource, "role") ) WITH bloom_filter_fp_chance = 0.01 AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' @@ -20,9 +20,9 @@ WITH bloom_filter_fp_chance = 0.01 AND speculative_retry = '99.0PERCENTILE'; CREATE TABLE system_auth.role_members ( - role text, + "role" text, member text, - PRIMARY KEY (role, member) + PRIMARY KEY ("role", member) ) WITH bloom_filter_fp_chance = 0.01 AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' @@ -39,10 +39,10 @@ WITH bloom_filter_fp_chance = 0.01 AND speculative_retry = '99.0PERCENTILE'; CREATE TABLE system_auth.role_permissions ( - role text, + "role" text, resource text, - permissions set, - PRIMARY KEY (role, resource) + "permissions" set, + PRIMARY KEY ("role", resource) ) WITH bloom_filter_fp_chance = 0.01 AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' @@ -58,8 +58,8 @@ WITH bloom_filter_fp_chance = 0.01 AND read_repair_chance = 0.0 AND speculative_retry = '99.0PERCENTILE'; -CREATE TABLE system_auth.roles ( - role text PRIMARY KEY, +CREATE TABLE system_auth."roles" ( + "role" text PRIMARY KEY, can_login boolean, is_superuser boolean, member_of set, @@ -262,7 +262,7 @@ WITH COMPACT STORAGE AND speculative_retry = '99.0PERCENTILE'; CREATE TABLE system.local ( - key text PRIMARY KEY, + "key" text PRIMARY KEY, bootstrapped text, broadcast_address inet, cluster_name text, @@ -386,7 +386,7 @@ CREATE TABLE system.schema_aggregates ( signature frozen >, argument_types list, final_func text, - initcond blob, + "initcond" blob, return_type text, state_func text, state_type text, @@ -432,7 +432,7 @@ CREATE TABLE system.schema_columnfamilies ( read_repair_chance double, speculative_retry text, subcomparator text, - type text, + "type" text, PRIMARY KEY (keyspace_name, columnfamily_name) ) WITH bloom_filter_fp_chance = 0.01 @@ -457,7 +457,7 @@ CREATE TABLE system.schema_columns ( index_name text, index_options text, index_type text, - type text, + "type" text, validator text, PRIMARY KEY (keyspace_name, columnfamily_name, column_name) ) @@ -483,7 +483,7 @@ CREATE TABLE system.schema_functions ( argument_types list, body text, called_on_null_input boolean, - language text, + "language" text, return_type text, PRIMARY KEY (keyspace_name, function_name, signature) ) @@ -591,8 +591,8 @@ CREATE TABLE system.sstable_activity ( keyspace_name text, columnfamily_name text, generation int, - rate_120m double, - rate_15m double, + "rate_120m" double, + "rate_15m" double, PRIMARY KEY ((keyspace_name, columnfamily_name, generation)) ) WITH bloom_filter_fp_chance = 0.01 @@ -656,4 +656,70 @@ WITH bloom_filter_fp_chance = 0.01 AND memtable_flush_period_in_ms = 3600000 AND min_index_interval = 128 AND read_repair_chance = 0.0 - AND speculative_retry = '99.0PERCENTILE'; \ No newline at end of file + AND speculative_retry = '99.0PERCENTILE'; + +CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE simplex."t1" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' + AND comment = 'test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 3600000 + AND min_index_interval = 128 + AND read_repair_chance = 0.1 + AND speculative_retry = '99.0PERCENTILE'; + +CREATE INDEX "ind1" ON simplex."t1" ("f2"); + +CREATE TRIGGER mytrigger ON simplex."t1" USING 'org.apache.cassandra.triggers.AuditTrigger'; + +CREATE TABLE simplex."t2" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH CLUSTERING ORDER BY ("f2" DESC) + AND COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' + AND comment = 'compact test table' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 3600000 + AND min_index_interval = 128 + AND read_repair_chance = 0.1 + AND speculative_retry = '99.0PERCENTILE'; + +CREATE TABLE simplex."t3" ( + "f1" int PRIMARY KEY, + "f2" int +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}' + AND comment = 'test table one column key' + AND compaction = {'class': 'SizeTieredCompactionStrategy'} + AND compression = {'sstable_compression': 'LZ4Compressor'} + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 1 + AND gc_grace_seconds = 2 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 3600000 + AND min_index_interval = 128 + AND read_repair_chance = 0.1 + AND speculative_retry = '99.0PERCENTILE'; diff --git a/spec/cassandra/cluster/schema/fetchers/3.0.0-beta2-data.json b/spec/cassandra/cluster/schema/fetchers/3.0.0-beta2-data.json deleted file mode 100644 index 9d666f8e5..000000000 --- a/spec/cassandra/cluster/schema/fetchers/3.0.0-beta2-data.json +++ /dev/null @@ -1,2950 +0,0 @@ -{ - "SELECT * FROM system_schema.keyspaces": [ - { - "keyspace_name": "system_auth", - "durable_writes": true, - "replication": { - "class": "org.apache.cassandra.locator.SimpleStrategy", - "replication_factor": "1" - } - }, - { - "keyspace_name": "system_schema", - "durable_writes": true, - "replication": { - "class": "org.apache.cassandra.locator.LocalStrategy" - } - }, - { - "keyspace_name": "system_distributed", - "durable_writes": true, - "replication": { - "class": "org.apache.cassandra.locator.SimpleStrategy", - "replication_factor": "3" - } - }, - { - "keyspace_name": "system", - "durable_writes": true, - "replication": { - "class": "org.apache.cassandra.locator.LocalStrategy" - } - }, - { - "keyspace_name": "system_traces", - "durable_writes": true, - "replication": { - "class": "org.apache.cassandra.locator.SimpleStrategy", - "replication_factor": "2" - } - } - ], - "SELECT * FROM system_schema.tables": [ - { - "keyspace_name": "system_auth", - "table_name": "resource_role_permissons_index", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "index of db roles with permissions granted on a resource", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 7776000, - "id": "5f2fbdad-91f1-3946-bd25-d5da3a5c35ec", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_members", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "role memberships lookup table", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 7776000, - "id": "0ecdaa87-f8fb-3e60-88d1-74fb36fe5c0d", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_permissions", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "permissions granted to db roles", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 7776000, - "id": "3afbe79f-2194-31a7-add7-f5ab90d8ec9c", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_auth", - "table_name": "roles", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "role definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 7776000, - "id": "5bc52802-de25-35ed-aeab-188eecebb090", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "user defined aggregate definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "924c5587-2e3a-345b-b10c-12f37c1ba895", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "column definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "24101c25-a2ae-3af7-87c1-b40ee1aca33f", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "dropped_columns", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "dropped column registry", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "5e7583b5-f3f4-3af1-9a39-b7e1d6f5f11f", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "user defined function definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "96489b79-80be-3e14-a701-66a0b9159450", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "secondary index definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "0feb57ac-311f-382f-ba6d-9024d305702f", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "keyspaces", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "keyspace definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "abac5682-dea6-31c5-b535-b3d6cffd0fb6", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "materialized views definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "20b0f171-36aa-399a-b920-583ca6fa31bf", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "table definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "afddfb9d-bc1e-3068-8056-eed6c302ba09", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "triggers", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "trigger definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "4df70b66-6b05-3251-95a1-32b54005fd48", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_schema", - "table_name": "types", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "user defined type definitions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 604800, - "id": "5a8b1ca8-6602-3f77-a045-9273d308917a", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "Repair history", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "deabd734-b99d-3b9c-92e5-fd92eb5abf14", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "Repair history", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "759fffad-624b-3181-80ee-fa9a52d1f627", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "IndexInfo", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "built column indexes", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "dense" - ], - "gc_grace_seconds": 0, - "id": "9f5c6374-d485-3229-9a0a-5094af9ad1e3", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "available_ranges", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "available keyspace/ranges during bootstrap/replace that are ready to be served", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "c539fcab-d65a-31d1-8133-d25605643ee3", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "batches", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "batches awaiting replay", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "2" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "919a4bc5-7a33-3573-b03e-13fc3f68b465", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "batchlog", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "*DEPRECATED* batchlog entries", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "2" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "0290003c-977e-397c-ac3e-fdfdc01d626b", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "built_materialized_views", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "built materialized views", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "08eb3b92-7c33-36b4-9e17-292203594304", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "week-long compaction history", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 604800, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "b4dbb7b4-dc49-3fb5-b3bf-ce6e434832ca", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "hints", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "*DEPRECATED* hints awaiting delivery", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "enabled": "false", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound", - "dense" - ], - "gc_grace_seconds": 0, - "id": "2666e205-73ef-38b3-90fe-fecf96e8f0c7", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "local", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "information about the local node", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "7ad54392-bcdd-35a6-8417-4e047860b377", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "materialized_views_builds_in_progress", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "materialized views builds current progress", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "1303e99a-d343-3fdb-bdfe-086aa9c6797d", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "in-progress paxos proposals", - "compaction": { - "class": "org.apache.cassandra.db.compaction.LeveledCompactionStrategy" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "b7b7f0c2-fd0a-3410-8c05-3ef614bb7c2d", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "peer_events", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "events related to peers", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "59dfeaea-8db2-3341-91ef-109974d81484", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "information about known peers in the cluster", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "37f71aca-7dc2-383b-a706-72528af04d4f", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "range_xfers", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "ranges requested for transfer", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "55d76438-4e55-3f8b-9f6e-676d4af3976d", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "per-table primary range size estimates", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "618f817b-005f-3678-b8a4-53f3930b8e86", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system", - "table_name": "sstable_activity", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "historic sstable read rates", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "5a1ff267-ace0-3f12-8563-cfae6103c65e", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "tracing events", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "8826e8e9-e16a-3728-8753-3bc1fc713c25", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "bloom_filter_fp_chance": 0.01, - "caching": { - "keys": "ALL", - "rows_per_partition": "NONE" - }, - "comment": "tracing sessions", - "compaction": { - "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", - "max_threshold": "32", - "min_threshold": "4" - }, - "compression": { - "chunk_length_in_kb": "64", - "class": "org.apache.cassandra.io.compress.LZ4Compressor" - }, - "dclocal_read_repair_chance": 0.0, - "default_time_to_live": 0, - "extensions": { - }, - "flags": [ - "compound" - ], - "gc_grace_seconds": 0, - "id": "c5e99f16-8677-3914-b17e-960613512345", - "max_index_interval": 2048, - "memtable_flush_period_in_ms": 3600000, - "min_index_interval": 128, - "read_repair_chance": 0.0, - "speculative_retry": "99PERCENTILE" - } - ], - "SELECT * FROM system_schema.columns": [ - { - "keyspace_name": "system_auth", - "table_name": "resource_role_permissons_index", - "column_name": "resource", - "column_name_bytes": "resource", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "resource_role_permissons_index", - "column_name": "role", - "column_name_bytes": "role", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_members", - "column_name": "member", - "column_name_bytes": "member", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_members", - "column_name": "role", - "column_name_bytes": "role", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_permissions", - "column_name": "permissions", - "column_name_bytes": "permissions", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_permissions", - "column_name": "resource", - "column_name_bytes": "resource", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "role_permissions", - "column_name": "role", - "column_name_bytes": "role", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "roles", - "column_name": "can_login", - "column_name_bytes": "can_login", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BooleanType" - }, - { - "keyspace_name": "system_auth", - "table_name": "roles", - "column_name": "is_superuser", - "column_name_bytes": "is_superuser", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BooleanType" - }, - { - "keyspace_name": "system_auth", - "table_name": "roles", - "column_name": "member_of", - "column_name_bytes": "member_of", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system_auth", - "table_name": "roles", - "column_name": "role", - "column_name_bytes": "role", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_auth", - "table_name": "roles", - "column_name": "salted_hash", - "column_name_bytes": "salted_hash", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "aggregate_name", - "column_name_bytes": "aggregate_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "argument_types", - "column_name_bytes": "argument_types", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "final_func", - "column_name_bytes": "final_func", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "initcond", - "column_name_bytes": "initcond", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "return_type", - "column_name_bytes": "return_type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "signature", - "column_name_bytes": "signature", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "state_func", - "column_name_bytes": "state_func", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "aggregates", - "column_name": "state_type", - "column_name_bytes": "state_type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "column_name", - "column_name_bytes": "column_name", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "column_name_bytes", - "column_name_bytes": "column_name_bytes", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "component_index", - "column_name_bytes": "component_index", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "type", - "column_name_bytes": "type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "columns", - "column_name": "validator", - "column_name_bytes": "validator", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "dropped_columns", - "column_name": "column_name", - "column_name_bytes": "column_name", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "dropped_columns", - "column_name": "dropped_time", - "column_name_bytes": "dropped_time", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system_schema", - "table_name": "dropped_columns", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "dropped_columns", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "dropped_columns", - "column_name": "type", - "column_name_bytes": "type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "argument_names", - "column_name_bytes": "argument_names", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "argument_types", - "column_name_bytes": "argument_types", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "body", - "column_name_bytes": "body", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "called_on_null_input", - "column_name_bytes": "called_on_null_input", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BooleanType" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "function_name", - "column_name_bytes": "function_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "language", - "column_name_bytes": "language", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "return_type", - "column_name_bytes": "return_type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "functions", - "column_name": "signature", - "column_name_bytes": "signature", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "index_name", - "column_name_bytes": "index_name", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "index_type", - "column_name_bytes": "index_type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "options", - "column_name_bytes": "options", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "target_columns", - "column_name_bytes": "target_columns", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "indexes", - "column_name": "target_type", - "column_name_bytes": "target_type", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "keyspaces", - "column_name": "durable_writes", - "column_name_bytes": "durable_writes", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BooleanType" - }, - { - "keyspace_name": "system_schema", - "table_name": "keyspaces", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "keyspaces", - "column_name": "replication", - "column_name_bytes": "replication", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "column_name": "clustering_columns", - "column_name_bytes": "clustering_columns", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "column_name": "included_columns", - "column_name_bytes": "included_columns", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "column_name": "target_columns", - "column_name_bytes": "target_columns", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "materialized_views", - "column_name": "view_name", - "column_name_bytes": "view_name", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "bloom_filter_fp_chance", - "column_name_bytes": "bloom_filter_fp_chance", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.DoubleType" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "caching", - "column_name_bytes": "caching", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "comment", - "column_name_bytes": "comment", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "compaction", - "column_name_bytes": "compaction", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "compression", - "column_name_bytes": "compression", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "dclocal_read_repair_chance", - "column_name_bytes": "dclocal_read_repair_chance", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.DoubleType" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "default_time_to_live", - "column_name_bytes": "default_time_to_live", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "extensions", - "column_name_bytes": "extensions", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.BytesType))" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "flags", - "column_name_bytes": "flags", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "gc_grace_seconds", - "column_name_bytes": "gc_grace_seconds", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "id", - "column_name_bytes": "id", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "max_index_interval", - "column_name_bytes": "max_index_interval", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "memtable_flush_period_in_ms", - "column_name_bytes": "memtable_flush_period_in_ms", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "min_index_interval", - "column_name_bytes": "min_index_interval", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "read_repair_chance", - "column_name_bytes": "read_repair_chance", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.DoubleType" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "speculative_retry", - "column_name_bytes": "speculative_retry", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "tables", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "triggers", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "triggers", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "triggers", - "column_name": "trigger_name", - "column_name_bytes": "trigger_name", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "triggers", - "column_name": "trigger_options", - "column_name_bytes": "trigger_options", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "types", - "column_name": "field_names", - "column_name_bytes": "field_names", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "types", - "column_name": "field_types", - "column_name_bytes": "field_types", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))" - }, - { - "keyspace_name": "system_schema", - "table_name": "types", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_schema", - "table_name": "types", - "column_name": "type_name", - "column_name_bytes": "type_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "columnfamily_names", - "column_name_bytes": "columnfamily_names", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "exception_message", - "column_name_bytes": "exception_message", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "exception_stacktrace", - "column_name_bytes": "exception_stacktrace", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "finished_at", - "column_name_bytes": "finished_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "parent_id", - "column_name_bytes": "parent_id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "requested_ranges", - "column_name_bytes": "requested_ranges", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "started_at", - "column_name_bytes": "started_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "parent_repair_history", - "column_name": "successful_ranges", - "column_name_bytes": "successful_ranges", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "columnfamily_name", - "column_name_bytes": "columnfamily_name", - "component_index": 1, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "coordinator", - "column_name_bytes": "coordinator", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "exception_message", - "column_name_bytes": "exception_message", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "exception_stacktrace", - "column_name_bytes": "exception_stacktrace", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "finished_at", - "column_name_bytes": "finished_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "id", - "column_name_bytes": "id", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": 0, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "parent_id", - "column_name_bytes": "parent_id", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "participants", - "column_name_bytes": "participants", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.InetAddressType)" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "range_begin", - "column_name_bytes": "range_begin", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "range_end", - "column_name_bytes": "range_end", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "started_at", - "column_name_bytes": "started_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system_distributed", - "table_name": "repair_history", - "column_name": "status", - "column_name_bytes": "status", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "IndexInfo", - "column_name": "index_name", - "column_name_bytes": "index_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "IndexInfo", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "IndexInfo", - "column_name": "value", - "column_name_bytes": "value", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.EmptyType" - }, - { - "keyspace_name": "system", - "table_name": "available_ranges", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "available_ranges", - "column_name": "ranges", - "column_name_bytes": "ranges", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.BytesType)" - }, - { - "keyspace_name": "system", - "table_name": "batches", - "column_name": "id", - "column_name_bytes": "id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system", - "table_name": "batches", - "column_name": "mutations", - "column_name_bytes": "mutations", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.BytesType)" - }, - { - "keyspace_name": "system", - "table_name": "batches", - "column_name": "version", - "column_name_bytes": "version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "batchlog", - "column_name": "data", - "column_name_bytes": "data", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system", - "table_name": "batchlog", - "column_name": "id", - "column_name_bytes": "id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "batchlog", - "column_name": "version", - "column_name_bytes": "version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "batchlog", - "column_name": "written_at", - "column_name_bytes": "written_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system", - "table_name": "built_materialized_views", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "built_materialized_views", - "column_name": "view_name", - "column_name_bytes": "view_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "bytes_in", - "column_name_bytes": "bytes_in", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.LongType" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "bytes_out", - "column_name_bytes": "bytes_out", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.LongType" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "columnfamily_name", - "column_name_bytes": "columnfamily_name", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "compacted_at", - "column_name_bytes": "compacted_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "id", - "column_name_bytes": "id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "compaction_history", - "column_name": "rows_merged", - "column_name_bytes": "rows_merged", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.LongType)" - }, - { - "keyspace_name": "system", - "table_name": "hints", - "column_name": "hint_id", - "column_name_bytes": "hint_id", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system", - "table_name": "hints", - "column_name": "message_version", - "column_name_bytes": "message_version", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "hints", - "column_name": "mutation", - "column_name_bytes": "mutation", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system", - "table_name": "hints", - "column_name": "target_id", - "column_name_bytes": "target_id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "bootstrapped", - "column_name_bytes": "bootstrapped", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "broadcast_address", - "column_name_bytes": "broadcast_address", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "cluster_name", - "column_name_bytes": "cluster_name", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "cql_version", - "column_name_bytes": "cql_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "data_center", - "column_name_bytes": "data_center", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "gossip_generation", - "column_name_bytes": "gossip_generation", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "host_id", - "column_name_bytes": "host_id", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "key", - "column_name_bytes": "key", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "listen_address", - "column_name_bytes": "listen_address", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "native_protocol_version", - "column_name_bytes": "native_protocol_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "partitioner", - "column_name_bytes": "partitioner", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "rack", - "column_name_bytes": "rack", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "release_version", - "column_name_bytes": "release_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "rpc_address", - "column_name_bytes": "rpc_address", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "schema_version", - "column_name_bytes": "schema_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "thrift_version", - "column_name_bytes": "thrift_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "tokens", - "column_name_bytes": "tokens", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system", - "table_name": "local", - "column_name": "truncated_at", - "column_name_bytes": "truncated_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UUIDType,org.apache.cassandra.db.marshal.BytesType)" - }, - { - "keyspace_name": "system", - "table_name": "materialized_views_builds_in_progress", - "column_name": "generation_number", - "column_name_bytes": "generation_number", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "materialized_views_builds_in_progress", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "materialized_views_builds_in_progress", - "column_name": "last_token", - "column_name_bytes": "last_token", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "materialized_views_builds_in_progress", - "column_name": "view_name", - "column_name_bytes": "view_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "cf_id", - "column_name_bytes": "cf_id", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "in_progress_ballot", - "column_name_bytes": "in_progress_ballot", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "most_recent_commit", - "column_name_bytes": "most_recent_commit", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "most_recent_commit_at", - "column_name_bytes": "most_recent_commit_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "most_recent_commit_version", - "column_name_bytes": "most_recent_commit_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "proposal", - "column_name_bytes": "proposal", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "proposal_ballot", - "column_name_bytes": "proposal_ballot", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "proposal_version", - "column_name_bytes": "proposal_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "paxos", - "column_name": "row_key", - "column_name_bytes": "row_key", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system", - "table_name": "peer_events", - "column_name": "hints_dropped", - "column_name_bytes": "hints_dropped", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UUIDType,org.apache.cassandra.db.marshal.Int32Type)" - }, - { - "keyspace_name": "system", - "table_name": "peer_events", - "column_name": "peer", - "column_name_bytes": "peer", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "data_center", - "column_name_bytes": "data_center", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "host_id", - "column_name_bytes": "host_id", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "peer", - "column_name_bytes": "peer", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "preferred_ip", - "column_name_bytes": "preferred_ip", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "rack", - "column_name_bytes": "rack", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "release_version", - "column_name_bytes": "release_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "rpc_address", - "column_name_bytes": "rpc_address", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "schema_version", - "column_name_bytes": "schema_version", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system", - "table_name": "peers", - "column_name": "tokens", - "column_name_bytes": "tokens", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system", - "table_name": "range_xfers", - "column_name": "requested_at", - "column_name_bytes": "requested_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - }, - { - "keyspace_name": "system", - "table_name": "range_xfers", - "column_name": "token_bytes", - "column_name_bytes": "token_bytes", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.BytesType" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "column_name": "mean_partition_size", - "column_name_bytes": "mean_partition_size", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.LongType" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "column_name": "partitions_count", - "column_name_bytes": "partitions_count", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.LongType" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "column_name": "range_end", - "column_name_bytes": "range_end", - "component_index": 2, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "column_name": "range_start", - "column_name_bytes": "range_start", - "component_index": 1, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "size_estimates", - "column_name": "table_name", - "column_name_bytes": "table_name", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "sstable_activity", - "column_name": "columnfamily_name", - "column_name_bytes": "columnfamily_name", - "component_index": 1, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "sstable_activity", - "column_name": "generation", - "column_name_bytes": "generation", - "component_index": 2, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system", - "table_name": "sstable_activity", - "column_name": "keyspace_name", - "column_name_bytes": "keyspace_name", - "component_index": 0, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system", - "table_name": "sstable_activity", - "column_name": "rate_120m", - "column_name_bytes": "rate_120m", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.DoubleType" - }, - { - "keyspace_name": "system", - "table_name": "sstable_activity", - "column_name": "rate_15m", - "column_name_bytes": "rate_15m", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.DoubleType" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "column_name": "activity", - "column_name_bytes": "activity", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "column_name": "event_id", - "column_name_bytes": "event_id", - "component_index": 0, - "type": "clustering", - "validator": "org.apache.cassandra.db.marshal.TimeUUIDType" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "column_name": "session_id", - "column_name_bytes": "session_id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "column_name": "source", - "column_name_bytes": "source", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "column_name": "source_elapsed", - "column_name_bytes": "source_elapsed", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_traces", - "table_name": "events", - "column_name": "thread", - "column_name_bytes": "thread", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "client", - "column_name_bytes": "client", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "command", - "column_name_bytes": "command", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "coordinator", - "column_name_bytes": "coordinator", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.InetAddressType" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "duration", - "column_name_bytes": "duration", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.Int32Type" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "parameters", - "column_name_bytes": "parameters", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "request", - "column_name_bytes": "request", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.UTF8Type" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "session_id", - "column_name_bytes": "session_id", - "component_index": null, - "type": "partition_key", - "validator": "org.apache.cassandra.db.marshal.UUIDType" - }, - { - "keyspace_name": "system_traces", - "table_name": "sessions", - "column_name": "started_at", - "column_name_bytes": "started_at", - "component_index": null, - "type": "regular", - "validator": "org.apache.cassandra.db.marshal.TimestampType" - } - ], - "SELECT * FROM system_schema.types": [ - - ], - "SELECT * FROM system_schema.functions": [ - - ], - "SELECT * FROM system_schema.aggregates": [ - - ], - "SELECT * FROM system_schema.indexes": [ - - ], - "SELECT * FROM system_schema.materialized_views": [ - - ] -} \ No newline at end of file diff --git a/spec/cassandra/cluster/schema/fetchers/3.0.0-beta2-schema.cql b/spec/cassandra/cluster/schema/fetchers/3.0.0-beta2-schema.cql deleted file mode 100644 index 3b21b80e5..000000000 --- a/spec/cassandra/cluster/schema/fetchers/3.0.0-beta2-schema.cql +++ /dev/null @@ -1,760 +0,0 @@ -CREATE KEYSPACE system_auth WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; - -CREATE TABLE system_auth.resource_role_permissons_index ( - resource text, - role text, - PRIMARY KEY (resource, role) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'index of db roles with permissions granted on a resource' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 7776000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_auth.role_members ( - role text, - member text, - PRIMARY KEY (role, member) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'role memberships lookup table' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 7776000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_auth.role_permissions ( - role text, - resource text, - permissions set, - PRIMARY KEY (role, resource) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'permissions granted to db roles' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 7776000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_auth.roles ( - role text PRIMARY KEY, - can_login boolean, - is_superuser boolean, - member_of set, - salted_hash text -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'role definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 7776000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE system_schema WITH replication = {'class': 'LocalStrategy'} AND durable_writes = true; - -CREATE TABLE system_schema.aggregates ( - keyspace_name text, - aggregate_name text, - signature frozen >, - argument_types frozen >, - final_func text, - initcond blob, - return_type text, - state_func text, - state_type text, - PRIMARY KEY (keyspace_name, aggregate_name, signature) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'user defined aggregate definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.columns ( - keyspace_name text, - table_name text, - column_name text, - column_name_bytes blob, - component_index int, - type text, - validator text, - PRIMARY KEY (keyspace_name, table_name, column_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'column definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.dropped_columns ( - keyspace_name text, - table_name text, - column_name text, - dropped_time timestamp, - type text, - PRIMARY KEY (keyspace_name, table_name, column_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'dropped column registry' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.functions ( - keyspace_name text, - function_name text, - signature frozen >, - argument_names frozen >, - argument_types frozen >, - body text, - called_on_null_input boolean, - language text, - return_type text, - PRIMARY KEY (keyspace_name, function_name, signature) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'user defined function definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.indexes ( - keyspace_name text, - table_name text, - index_name text, - index_type text, - options frozen >, - target_columns frozen >, - target_type text, - PRIMARY KEY (keyspace_name, table_name, index_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'secondary index definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.keyspaces ( - keyspace_name text PRIMARY KEY, - durable_writes boolean, - replication frozen > -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'keyspace definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.materialized_views ( - keyspace_name text, - table_name text, - view_name text, - clustering_columns frozen >, - included_columns frozen >, - target_columns frozen >, - PRIMARY KEY (keyspace_name, table_name, view_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'materialized views definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.tables ( - keyspace_name text, - table_name text, - bloom_filter_fp_chance double, - caching frozen >, - comment text, - compaction frozen >, - compression frozen >, - dclocal_read_repair_chance double, - default_time_to_live int, - extensions frozen >, - flags frozen >, - gc_grace_seconds int, - id uuid, - max_index_interval int, - memtable_flush_period_in_ms int, - min_index_interval int, - read_repair_chance double, - speculative_retry text, - PRIMARY KEY (keyspace_name, table_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'table definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.triggers ( - keyspace_name text, - table_name text, - trigger_name text, - trigger_options frozen >, - PRIMARY KEY (keyspace_name, table_name, trigger_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'trigger definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_schema.types ( - keyspace_name text, - type_name text, - field_names frozen >, - field_types frozen >, - PRIMARY KEY (keyspace_name, type_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'user defined type definitions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 604800 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE system_distributed WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'} AND durable_writes = true; - -CREATE TABLE system_distributed.parent_repair_history ( - parent_id timeuuid PRIMARY KEY, - columnfamily_names set, - exception_message text, - exception_stacktrace text, - finished_at timestamp, - keyspace_name text, - requested_ranges set, - started_at timestamp, - successful_ranges set -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'Repair history' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_distributed.repair_history ( - keyspace_name text, - columnfamily_name text, - id timeuuid, - coordinator inet, - exception_message text, - exception_stacktrace text, - finished_at timestamp, - parent_id timeuuid, - participants set, - range_begin text, - range_end text, - started_at timestamp, - status text, - PRIMARY KEY ((keyspace_name, columnfamily_name), id) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'Repair history' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE system WITH replication = {'class': 'LocalStrategy'} AND durable_writes = true; - -CREATE TABLE system."IndexInfo" ( - table_name text, - index_name text, - value 'org.apache.cassandra.db.marshal.EmptyType', - PRIMARY KEY (table_name, index_name) -) -WITH COMPACT STORAGE - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'built column indexes' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.available_ranges ( - keyspace_name text PRIMARY KEY, - ranges set -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'available keyspace/ranges during bootstrap/replace that are ready to be served' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.batches ( - id timeuuid PRIMARY KEY, - mutations list, - version int -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'batches awaiting replay' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '2'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.batchlog ( - id uuid PRIMARY KEY, - data blob, - version int, - written_at timestamp -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '*DEPRECATED* batchlog entries' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '2'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.built_materialized_views ( - keyspace_name text, - view_name text, - PRIMARY KEY (keyspace_name, view_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'built materialized views' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.compaction_history ( - id uuid PRIMARY KEY, - bytes_in bigint, - bytes_out bigint, - columnfamily_name text, - compacted_at timestamp, - keyspace_name text, - rows_merged map -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'week-long compaction history' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 604800 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.hints ( - target_id uuid, - hint_id timeuuid, - message_version int, - mutation blob, - PRIMARY KEY (target_id, hint_id, message_version) -) -WITH COMPACT STORAGE - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '*DEPRECATED* hints awaiting delivery' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'enabled': 'false', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.local ( - key text PRIMARY KEY, - bootstrapped text, - broadcast_address inet, - cluster_name text, - cql_version text, - data_center text, - gossip_generation int, - host_id uuid, - listen_address inet, - native_protocol_version text, - partitioner text, - rack text, - release_version text, - rpc_address inet, - schema_version uuid, - thrift_version text, - tokens set, - truncated_at map -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'information about the local node' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.materialized_views_builds_in_progress ( - keyspace_name text, - view_name text, - generation_number int, - last_token text, - PRIMARY KEY (keyspace_name, view_name) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'materialized views builds current progress' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.paxos ( - row_key blob, - cf_id uuid, - in_progress_ballot timeuuid, - most_recent_commit blob, - most_recent_commit_at timeuuid, - most_recent_commit_version int, - proposal blob, - proposal_ballot timeuuid, - proposal_version int, - PRIMARY KEY (row_key, cf_id) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'in-progress paxos proposals' - AND compaction = {'class': 'LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.peer_events ( - peer inet PRIMARY KEY, - hints_dropped map -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'events related to peers' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.peers ( - peer inet PRIMARY KEY, - data_center text, - host_id uuid, - preferred_ip inet, - rack text, - release_version text, - rpc_address inet, - schema_version uuid, - tokens set -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'information about known peers in the cluster' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.range_xfers ( - token_bytes blob PRIMARY KEY, - requested_at timestamp -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'ranges requested for transfer' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.size_estimates ( - keyspace_name text, - table_name text, - range_start text, - range_end text, - mean_partition_size bigint, - partitions_count bigint, - PRIMARY KEY (keyspace_name, table_name, range_start, range_end) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'per-table primary range size estimates' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system.sstable_activity ( - keyspace_name text, - columnfamily_name text, - generation int, - rate_120m double, - rate_15m double, - PRIMARY KEY ((keyspace_name, columnfamily_name, generation)) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'historic sstable read rates' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE system_traces WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '2'} AND durable_writes = true; - -CREATE TABLE system_traces.events ( - session_id uuid, - event_id timeuuid, - activity text, - source inet, - source_elapsed int, - thread text, - PRIMARY KEY (session_id, event_id) -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'tracing events' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE system_traces.sessions ( - session_id uuid PRIMARY KEY, - client inet, - command text, - coordinator inet, - duration int, - parameters map, - request text, - started_at timestamp -) -WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = 'tracing sessions' - AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} - AND dclocal_read_repair_chance = 0.0 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 3600000 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; \ No newline at end of file diff --git a/spec/cassandra/cluster/schema/fetchers/3.0.0-data.json b/spec/cassandra/cluster/schema/fetchers/3.0.0-data.json new file mode 100644 index 000000000..495b3cd2c --- /dev/null +++ b/spec/cassandra/cluster/schema/fetchers/3.0.0-data.json @@ -0,0 +1,411 @@ +{ + "SELECT * FROM system_schema.keyspaces": [ + { + "keyspace_name": "simplex", + "durable_writes": true, + "replication": { + "class": "org.apache.cassandra.locator.SimpleStrategy", + "replication_factor": "1" + } + } + ], + "SELECT * FROM system_schema.tables": [ + { + "keyspace_name": "simplex", + "table_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": { + "keys": "ALL", + "rows_per_partition": "NONE" + }, + "cdc": true, + "comment": "test table for unit tests", + "compaction": { + "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "max_threshold": "32", + "min_threshold": "4" + }, + "compression": { + "chunk_length_in_kb": "64", + "class": "org.apache.cassandra.io.compress.LZ4Compressor" + }, + "crc_check_chance": 1.0, + "dclocal_read_repair_chance": 0.1, + "default_time_to_live": 0, + "extensions": { + "object_type": "ext table" + }, + "flags": [ + "compound" + ], + "gc_grace_seconds": 7776000, + "id": "5f2fbdad-91f1-3946-bd25-d5da3a5c35ec", + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 42, + "min_index_interval": 128, + "read_repair_chance": 0.0, + "speculative_retry": "99PERCENTILE" + }, + { + "keyspace_name": "simplex", + "table_name": "t2", + "bloom_filter_fp_chance": 0.01, + "caching": { + "keys": "ALL", + "rows_per_partition": "NONE" + }, + "comment": "compact (dense) test table for unit tests", + "compaction": { + "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "max_threshold": "32", + "min_threshold": "4" + }, + "compression": { + "chunk_length_in_kb": "64", + "class": "org.apache.cassandra.io.compress.LZ4Compressor" + }, + "crc_check_chance": 1.0, + "dclocal_read_repair_chance": 0.1, + "default_time_to_live": 0, + "extensions": { + "object_type": "ext table" + }, + "flags": ["dense"], + "gc_grace_seconds": 7776000, + "id": "5f2fbdad-01f1-3946-bd25-d5da3a5c35ec", + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 42, + "min_index_interval": 128, + "read_repair_chance": 0.0, + "speculative_retry": "99PERCENTILE" + }, + { + "keyspace_name": "simplex", + "table_name": "t3", + "bloom_filter_fp_chance": 0.01, + "caching": { + "keys": "ALL", + "rows_per_partition": "NONE" + }, + "comment": "compact test table for unit tests", + "compaction": { + "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "max_threshold": "32", + "min_threshold": "4" + }, + "compression": { + "chunk_length_in_kb": "64", + "class": "org.apache.cassandra.io.compress.LZ4Compressor" + }, + "crc_check_chance": 1.0, + "dclocal_read_repair_chance": 0.1, + "default_time_to_live": 0, + "extensions": { + "object_type": "ext table" + }, + "flags": [], + "gc_grace_seconds": 7776000, + "id": "5f2fbdad-91f1-4946-bd25-d5da3a5c35ec", + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 42, + "min_index_interval": 128, + "read_repair_chance": 0.0, + "speculative_retry": "99PERCENTILE" + }, + { + "keyspace_name": "simplex", + "table_name": "t4", + "bloom_filter_fp_chance": 0.01, + "caching": { + "keys": "ALL", + "rows_per_partition": "NONE" + }, + "comment": "compact (dense) test table for unit tests", + "compaction": { + "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "max_threshold": "32", + "min_threshold": "4" + }, + "compression": { + "chunk_length_in_kb": "64", + "class": "org.apache.cassandra.io.compress.LZ4Compressor" + }, + "crc_check_chance": 1.0, + "dclocal_read_repair_chance": 0.1, + "default_time_to_live": 0, + "extensions": { + "object_type": "ext table" + }, + "flags": ["dense"], + "gc_grace_seconds": 7776000, + "id": "5f2fbdad-91f1-4946-bd35-d5da3a5c35ec", + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 42, + "min_index_interval": 128, + "read_repair_chance": 0.0, + "speculative_retry": "99PERCENTILE" + } + ], + "SELECT * FROM system_schema.columns": [ + { + "keyspace_name": "simplex", + "table_name": "t1", + "column_name": "f1", + "clustering_order": "none", + "column_name_bytes": "f1", + "kind": "partition_key", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t1", + "column_name": "f2", + "clustering_order": "asc", + "column_name_bytes": "f2", + "kind": "clustering", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t1", + "column_name": "f3", + "clustering_order": "none", + "column_name_bytes": "f3", + "kind": "regular", + "position": -1, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "v1", + "column_name": "f1", + "clustering_order": "none", + "column_name_bytes": "f1", + "kind": "partition_key", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "v1", + "column_name": "f2", + "clustering_order": "asc", + "column_name_bytes": "f2", + "kind": "clustering", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "v2", + "column_name": "f1", + "clustering_order": "none", + "column_name_bytes": "f1", + "kind": "partition_key", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "v2", + "column_name": "f2", + "clustering_order": "asc", + "column_name_bytes": "f2", + "kind": "clustering", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "v2", + "column_name": "f3", + "clustering_order": "none", + "column_name_bytes": "f3", + "kind": "regular", + "position": -1, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t2", + "column_name": "f1", + "clustering_order": "none", + "column_name_bytes": "f1", + "kind": "partition_key", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t2", + "column_name": "f2", + "clustering_order": "asc", + "column_name_bytes": "f2", + "kind": "clustering", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t2", + "column_name": "f3", + "clustering_order": "none", + "column_name_bytes": "f3", + "kind": "regular", + "position": -1, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t3", + "column_name": "f1", + "clustering_order": "none", + "column_name_bytes": "f1", + "kind": "partition_key", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t3", + "column_name": "f2", + "clustering_order": "none", + "column_name_bytes": "f2", + "kind": "static", + "position": -1, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t4", + "column_name": "f1", + "clustering_order": "none", + "column_name_bytes": "f1", + "kind": "partition_key", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t4", + "column_name": "f2", + "clustering_order": "desc", + "column_name_bytes": "f2", + "kind": "clustering", + "position": 0, + "type": "int" + }, + { + "keyspace_name": "simplex", + "table_name": "t4", + "column_name": "f3", + "clustering_order": "none", + "column_name_bytes": "f3", + "kind": "regular", + "position": -1, + "type": "int" + } + ], + "SELECT * FROM system_schema.types": [ + + ], + "SELECT * FROM system_schema.functions": [ + + ], + "SELECT * FROM system_schema.aggregates": [ + + ], + "SELECT * FROM system_schema.indexes": [ + { + "keyspace_name": "simplex", + "table_name": "t1", + "index_name": "ind1", + "kind": "COMPOSITES", + "options": {"target": "f2"} + } + ], + "SELECT * FROM system_schema.views": [ + { + "keyspace_name": "simplex", + "view_name": "v1", + "base_table_id": "5f2fbdad-91f1-3946-bd25-d5da3a5c35ec", + "base_table_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": { + "keys": "ALL", + "rows_per_partition": "NONE" + }, + "comment": "test view for unit tests", + "compaction": { + "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "max_threshold": "32", + "min_threshold": "4" + }, + "compression": { + "chunk_length_in_kb": "64", + "class": "org.apache.cassandra.io.compress.LZ4Compressor" + }, + "crc_check_chance": 1.0, + "dclocal_read_repair_chance": 0.1, + "default_time_to_live": 0, + "extensions": { + "object_type": "ext view" + }, + "gc_grace_seconds": 7776000, + "id": "4f2fbdad-91f1-3946-bd25-d5da3a5c35ec", + "include_all_columns": false, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 42, + "min_index_interval": 128, + "read_repair_chance": 0.0, + "speculative_retry": "99PERCENTILE", + "where_clause": "f2 IS NOT NULL" + }, + { + "keyspace_name": "simplex", + "view_name": "v2", + "base_table_id": "5f2fbdad-91f1-3946-bd25-d5da3a5c35ec", + "base_table_name": "t1", + "bloom_filter_fp_chance": 0.01, + "caching": { + "keys": "ALL", + "rows_per_partition": "NONE" + }, + "comment": "test view for unit tests", + "compaction": { + "class": "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "max_threshold": "32", + "min_threshold": "4" + }, + "compression": { + "chunk_length_in_kb": "64", + "class": "org.apache.cassandra.io.compress.LZ4Compressor" + }, + "crc_check_chance": 1.0, + "dclocal_read_repair_chance": 0.1, + "default_time_to_live": 42, + "extensions": { + "object_type": "ext view" + }, + "gc_grace_seconds": 7776000, + "id": "4f2fbdad-91f1-3946-bd25-d5da3a5c35ed", + "include_all_columns": true, + "max_index_interval": 2048, + "memtable_flush_period_in_ms": 42, + "min_index_interval": 128, + "read_repair_chance": 0.2, + "speculative_retry": "99PERCENTILE", + "where_clause": "f2 IS NOT NULL" + } + ], + "SELECT * FROM system_schema.triggers": [ + { + "keyspace_name": "simplex", + "table_name": "t1", + "trigger_name": "mytrigger", + "options": { "class": "org.apache.cassandra.triggers.AuditTrigger"} + } + ] +} diff --git a/spec/cassandra/cluster/schema/fetchers/3.0.0-schema.cql b/spec/cassandra/cluster/schema/fetchers/3.0.0-schema.cql new file mode 100644 index 000000000..537510eea --- /dev/null +++ b/spec/cassandra/cluster/schema/fetchers/3.0.0-schema.cql @@ -0,0 +1,132 @@ +CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE simplex."t1" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = true + AND comment = 'test table for unit tests' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 7776000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 42 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE INDEX "ind1" ON simplex."t1" ("f2"); + +CREATE TRIGGER mytrigger ON simplex."t1" USING 'org.apache.cassandra.triggers.AuditTrigger'; + +CREATE TABLE simplex."t2" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = 'compact (dense) test table for unit tests' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 7776000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 42 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE simplex."t3" ( + "f1" int PRIMARY KEY, + "f2" int +) +WITH COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = 'compact test table for unit tests' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 7776000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 42 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE simplex."t4" ( + "f1" int, + "f2" int, + "f3" int, + PRIMARY KEY ("f1", "f2") +) +WITH CLUSTERING ORDER BY ("f2" DESC) + AND COMPACT STORAGE + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = 'compact (dense) test table for unit tests' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 7776000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 42 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE MATERIALIZED VIEW simplex."v1" AS +SELECT "f1", "f2" +FROM simplex."t1" +WHERE f2 IS NOT NULL +PRIMARY KEY (("f1"), "f2") +WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = 'test view for unit tests' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 7776000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 42 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE MATERIALIZED VIEW simplex."v2" AS +SELECT * +FROM simplex."t1" +WHERE f2 IS NOT NULL +PRIMARY KEY (("f1"), "f2") +WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = 'test view for unit tests' + AND compaction = {'class': 'SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 42 + AND gc_grace_seconds = 7776000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 42 + AND min_index_interval = 128 + AND read_repair_chance = 0.2 + AND speculative_retry = '99PERCENTILE'; diff --git a/spec/cassandra/cluster/schema/fetchers_spec.rb b/spec/cassandra/cluster/schema/fetchers_spec.rb index 472b1fb63..bacb96f3c 100644 --- a/spec/cassandra/cluster/schema/fetchers_spec.rb +++ b/spec/cassandra/cluster/schema/fetchers_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,32 +23,38 @@ class Cluster class Schema module Fetchers [ - ['1.2.19', V1_2_x], - ['2.0.16', V2_0_x], - ['2.1.9', V2_1_x], - ['2.2.1', V2_2_x], - # ['3.0.0-beta2', V3_0_x], - ].each do |(version, klass)| - data = JSON.load(File.read(File.dirname(__FILE__) + '/fetchers/' + version + '-data.json')) + ['1.2.19', V1_2_x, FQCNTypeParser], + ['2.0.16', V2_0_x, FQCNTypeParser], + ['2.1.9', V2_1_x, FQCNTypeParser], + ['2.2.1', V2_2_x, FQCNTypeParser], + ['3.0.0', V3_0_x, CQLTypeParser] + ].each do |(version, klass, parser_class)| describe(klass) do + let(:data) { JSON.load(File.read(File.dirname(__FILE__) + '/fetchers/' + version + '-data.json')) } let(:connection) { double('cql protocol handler') } - let(:schema_type_parser) { FQCNTypeParser.new } - let(:cluster_schema) { double('schema') } + let(:schema_type_parser) { + parser_class.new + } + let(:cluster_schema) { double('schema') } subject { klass.new(schema_type_parser, cluster_schema) } + before do + allow(cluster_schema).to receive(:keyspace).and_return(nil) + end + describe('#fetch') do before do allow(connection).to receive(:send_request) do |r| case r - when Protocol::QueryRequest - if data.include?(r.cql) - Ione::Future.resolved(Protocol::RowsResultResponse.new(nil, nil, data[r.cql], nil, nil, nil)) + when Protocol::QueryRequest + if data.include?(r.cql) + Ione::Future.resolved(Protocol::RowsResultResponse.new(nil, nil, data[r.cql], nil, nil, nil)) + else + raise "unsupported cql: #{r.cql}" + end else - raise "unsupported cql: #{r.cql}" - end - else - raise "unexpected request: #{r.inspect}" + raise "unexpected request: #{r.inspect}" end end end @@ -60,10 +66,36 @@ module Fetchers parts << keyspace.to_cql keyspace.each_table do |table| parts << table.to_cql + + table.each_index do |index| + parts << index.to_cql + end + + table.each_trigger do |trigger| + parts << trigger.to_cql + end + end + keyspace.each_materialized_view do |view| + parts << view.to_cql end end cql = parts.join("\n\n") - expect(File.read(File.dirname(__FILE__) + '/fetchers/' + version + '-schema.cql')).to eq(cql) + cql += "\n" + expect(cql).to eq(File.read(File.dirname(__FILE__) + '/fetchers/' + version + '-schema.cql')) + end + + if klass == V3_0_x + it 'reports table extensions properly' do + keyspaces = subject.fetch(connection).value + table = keyspaces.first.table('t1') + expect(table.options.extensions).to eq({ 'object_type' => 'ext table' }) + end + + it 'reports view extensions properly' do + keyspaces = subject.fetch(connection).value + view = keyspaces.first.materialized_view('v1') + expect(view.options.extensions).to eq({ 'object_type' => 'ext view' }) + end end end end diff --git a/spec/cassandra/cluster/schema/fqcn_type_parser_spec.rb b/spec/cassandra/cluster/schema/fqcn_type_parser_spec.rb index 50ff7e256..b62a5cde2 100644 --- a/spec/cassandra/cluster/schema/fqcn_type_parser_spec.rb +++ b/spec/cassandra/cluster/schema/fqcn_type_parser_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster/schema/partitioners/murmur3_spec.rb b/spec/cassandra/cluster/schema/partitioners/murmur3_spec.rb index de3947a7a..d4b2cc36e 100644 --- a/spec/cassandra/cluster/schema/partitioners/murmur3_spec.rb +++ b/spec/cassandra/cluster/schema/partitioners/murmur3_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster/schema/partitioners/random_spec.rb b/spec/cassandra/cluster/schema/partitioners/random_spec.rb index 366df13ec..469fa4369 100644 --- a/spec/cassandra/cluster/schema/partitioners/random_spec.rb +++ b/spec/cassandra/cluster/schema/partitioners/random_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster/schema/replication_strategies/network_topology_spec.rb b/spec/cassandra/cluster/schema/replication_strategies/network_topology_spec.rb index c66717c6a..0890fc4b6 100644 --- a/spec/cassandra/cluster/schema/replication_strategies/network_topology_spec.rb +++ b/spec/cassandra/cluster/schema/replication_strategies/network_topology_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster/schema/replication_strategies/none_spec.rb b/spec/cassandra/cluster/schema/replication_strategies/none_spec.rb index 6e97f97eb..f001ee77b 100644 --- a/spec/cassandra/cluster/schema/replication_strategies/none_spec.rb +++ b/spec/cassandra/cluster/schema/replication_strategies/none_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster/schema_spec.rb b/spec/cassandra/cluster/schema_spec.rb index 420e27738..88703d5a2 100644 --- a/spec/cassandra/cluster/schema_spec.rb +++ b/spec/cassandra/cluster/schema_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/cluster_spec.rb b/spec/cassandra/cluster_spec.rb index e451e985e..962afaf61 100644 --- a/spec/cassandra/cluster_spec.rb +++ b/spec/cassandra/cluster_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ module Cassandra }) } - let(:cluster) { Cluster.new(driver.logger, io_reactor, executor, control_connection, cluster_registry, driver.cluster_schema, driver.cluster_metadata, driver.execution_options, driver.connection_options, load_balancing_policy, driver.reconnection_policy, driver.retry_policy, driver.address_resolution_policy, driver.connector, driver.futures_factory) } + let(:cluster) { Cluster.new(driver.logger, io_reactor, executor, control_connection, cluster_registry, driver.cluster_schema, driver.cluster_metadata, driver.execution_options, driver.connection_options, driver.profile_manager, driver.reconnection_policy, driver.address_resolution_policy, driver.connector, driver.futures_factory, driver.timestamp_generator) } before do io_reactor.connection_options = driver.connection_options diff --git a/spec/cassandra/compression/common.rb b/spec/cassandra/compression/common.rb index b04846527..95696b888 100644 --- a/spec/cassandra/compression/common.rb +++ b/spec/cassandra/compression/common.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/compression/compressors/lz4_spec.rb b/spec/cassandra/compression/compressors/lz4_spec.rb index b75dab868..dfb54e46e 100644 --- a/spec/cassandra/compression/compressors/lz4_spec.rb +++ b/spec/cassandra/compression/compressors/lz4_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/compression/compressors/snappy_spec.rb b/spec/cassandra/compression/compressors/snappy_spec.rb index 668cf899a..0740841b5 100644 --- a/spec/cassandra/compression/compressors/snappy_spec.rb +++ b/spec/cassandra/compression/compressors/snappy_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/execution/options_spec.rb b/spec/cassandra/execution/options_spec.rb new file mode 100644 index 000000000..e24757801 --- /dev/null +++ b/spec/cassandra/execution/options_spec.rb @@ -0,0 +1,92 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +module Cassandra + module Execution + describe(Options) do + let(:load_balancing_policy) { double('lbp') } + let(:lbp2) { double('lbp2') } + let(:retry_policy) { double('retry_policy') } + let(:base_options) { + Options.new(timeout: 10, consistency: :one, load_balancing_policy: load_balancing_policy, + retry_policy: retry_policy) + } + + before do + [load_balancing_policy, lbp2].each do |policy| + allow(policy).to receive(:host_up) + allow(policy).to receive(:host_down) + allow(policy).to receive(:host_found) + allow(policy).to receive(:host_lost) + allow(policy).to receive(:setup) + allow(policy).to receive(:teardown) + allow(policy).to receive(:distance) + allow(policy).to receive(:plan) + end + + allow(retry_policy).to receive(:read_timeout) + allow(retry_policy).to receive(:write_timeout) + allow(retry_policy).to receive(:unavailable) + end + + context :initialize do + it 'should yell if load-balancing policy is invalid' do + ['junk', nil].each do |val| + expect { + Options.new(load_balancing_policy: val, retry_policy: retry_policy, consistency: :one) + }.to raise_error(ArgumentError) + end + end + + it 'should yell if retry policy is invalid' do + ['junk', nil].each do |val| + expect { + Options.new(load_balancing_policy: load_balancing_policy, retry_policy: val, consistency: :one) + }.to raise_error(ArgumentError) + end + end + end + + it 'should allow nil timeout to override base non-nil timeout option' do + result = Options.new({timeout: nil}, base_options) + expect(result.timeout).to be_nil + end + + it 'should non-nil timeout to override base non-nil timeout option' do + result = Options.new({timeout: 123}, base_options) + expect(result.timeout).to eq(123) + end + + it 'should not override base timeout if not specified' do + result = Options.new({}, base_options) + expect(result.timeout).to eq(10) + end + + it 'should override with execution-profile and simple attribute' do + profile = Profile.new(load_balancing_policy: lbp2, retry_policy: retry_policy, timeout: 21, consistency: :quorum) + result = base_options.override(profile, timeout: 42) + expect(result.load_balancing_policy).to be(lbp2) + expect(result.retry_policy).to be(retry_policy) + expect(result.consistency).to be(:quorum) + expect(result.timeout).to eq(42) + end + end + end +end diff --git a/spec/cassandra/execution/profile_manager_spec.rb b/spec/cassandra/execution/profile_manager_spec.rb new file mode 100644 index 000000000..0fc6a2f78 --- /dev/null +++ b/spec/cassandra/execution/profile_manager_spec.rb @@ -0,0 +1,97 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +module Cassandra + module Execution + describe(ProfileManager) do + let(:cluster) { double('cluster') } + let(:host) { double('host') } + let(:lbp1) { double('lbp1') } + let(:lbp2) { double('lbp2') } + let(:lbp3) { double('lbp3') } + let(:retry_policy) { double('retry_policy') } + let(:profile1) { Profile.new(load_balancing_policy: lbp1) } + let(:profile2) { Profile.new(load_balancing_policy: lbp2) } + let(:profile3) { Profile.new(load_balancing_policy: lbp3) } + let(:profile5) { Profile.new(load_balancing_policy: lbp1) } + let(:default_profile) { + Profile.new(load_balancing_policy: lbp1, retry_policy: retry_policy, consistency: :quorum, timeout: 12) + } + let(:subject) { + ProfileManager.new(default_profile, {p1: profile1, p2: profile2, p3: profile3, p5: profile5}) + } + let(:subject_with_custom_default) { + ProfileManager.new(default_profile, {default: profile2}) + } + + before do + [lbp1, lbp2, lbp3].each do |policy| + allow(policy).to receive(:host_up) + allow(policy).to receive(:host_down) + allow(policy).to receive(:host_found) + allow(policy).to receive(:host_lost) + allow(policy).to receive(:setup) + allow(policy).to receive(:teardown) + allow(policy).to receive(:distance) + allow(policy).to receive(:plan) + end + + allow(retry_policy).to receive(:read_timeout) + allow(retry_policy).to receive(:write_timeout) + allow(retry_policy).to receive(:unavailable) + end + + it 'should respect custom default profile' do + expect(subject_with_custom_default.profiles[:default]).to be(profile2) + end + + it 'should return unique list of lbps' do + expect(subject.load_balancing_policies.size).to eq(3) + expect(subject.load_balancing_policies.include?(lbp1)).to eq(true) + end + + context :distance do + it 'should return :local if any lbp shows a :local distance' do + expect(lbp1).to receive(:distance).with(host).and_return(:local) + expect(lbp2).to receive(:distance).with(host).and_return(:remote) + expect(lbp3).to receive(:distance).with(host).and_return(:ignore) + + expect(subject.distance(host)).to be(:local) + end + + it 'should return :remote if any lbp shows a :remote distance and none show :local' do + expect(lbp1).to receive(:distance).with(host).and_return(:ignore) + expect(lbp2).to receive(:distance).with(host).and_return(:remote) + expect(lbp3).to receive(:distance).with(host).and_return(:ignore) + + expect(subject.distance(host)).to be(:remote) + end + + it 'should return :ignore if all lbps ignore the host' do + expect(lbp1).to receive(:distance).with(host).and_return(:ignore) + expect(lbp2).to receive(:distance).with(host).and_return(:ignore) + expect(lbp3).to receive(:distance).with(host).and_return(:ignore) + + expect(subject.distance(host)).to be(:ignore) + end + end + end + end +end diff --git a/spec/cassandra/execution/profile_spec.rb b/spec/cassandra/execution/profile_spec.rb new file mode 100644 index 000000000..653216e8f --- /dev/null +++ b/spec/cassandra/execution/profile_spec.rb @@ -0,0 +1,92 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +module Cassandra + module Execution + describe Profile do + let(:lbp) { double('lbp') } + let(:lbp2) { double('lbp2') } + let(:retry_policy) { double('retry_policy') } + let(:retry_policy2) { double('retry_policy2') } + + before do + [lbp, lbp2].each do |policy| + allow(policy).to receive(:host_up) + allow(policy).to receive(:host_down) + allow(policy).to receive(:host_found) + allow(policy).to receive(:host_lost) + allow(policy).to receive(:setup) + allow(policy).to receive(:teardown) + allow(policy).to receive(:distance) + allow(policy).to receive(:plan) + end + + [retry_policy, retry_policy2].each do |policy| + allow(policy).to receive(:read_timeout) + allow(policy).to receive(:write_timeout) + allow(policy).to receive(:unavailable) + end + end + + context :initialize do + it 'should validate :load_balancing_policy option' do + expect(Profile.new(load_balancing_policy: lbp).load_balancing_policy).to eq(lbp) + ['junk', nil].each do |val| + expect { Profile.new(load_balancing_policy: val) }.to raise_error(ArgumentError) + end + end + + it 'should validate :retry_policy option' do + expect(Profile.new(retry_policy: retry_policy).retry_policy).to be(retry_policy) + expect { Profile.new(retry_policy: 'junk') }.to raise_error(ArgumentError) + expect { Profile.new(retry_policy: nil) }.to raise_error(ArgumentError) + end + + it 'should validate :timeout' do + ['a', -1, 0].each do |val| + expect { Profile.new(timeout: val) }.to raise_error(ArgumentError) + end + [0.5, 38, nil].each do |val| + expect(Profile.new(timeout: val).timeout).to eq(val) + end + end + + it 'should validate :consistency' do + Cassandra::CONSISTENCIES.each do |c| + expect(Profile.new(consistency: c).consistency).to eq(c) + end + expect { Profile.new(consistency: 'foo') }.to raise_error(ArgumentError) + end + + it 'should fall back to system defaults for unspecified attributes' do + p = Profile.new + expect(p.load_balancing_policy).to be_a(LoadBalancing::Policies::TokenAware) + expect(p.retry_policy).to be_a(Retry::Policies::Default) + expect(p.timeout).to eq(12) + expect(p.consistency).to be(:local_one) + end + + it 'should support nil timeout' do + expect(Profile.new(timeout: nil).timeout).to be_nil + end + end + end + end +end diff --git a/spec/cassandra/execution/trace_spec.rb b/spec/cassandra/execution/trace_spec.rb index 75006d382..a855cfd48 100644 --- a/spec/cassandra/execution/trace_spec.rb +++ b/spec/cassandra/execution/trace_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,20 @@ module Execution describe(Trace) do let(:id) { Uuid::Generator.new.now } let(:client) { double('client') } - let(:trace) { Trace.new(id, client) } + let(:load_balancing_policy) { double('lbp') } + let(:trace) { Trace.new(id, client, load_balancing_policy) } + let(:execution_options) { VOID_OPTIONS.override(load_balancing_policy: load_balancing_policy) } + + before do + allow(load_balancing_policy).to receive(:host_up) + allow(load_balancing_policy).to receive(:host_down) + allow(load_balancing_policy).to receive(:host_found) + allow(load_balancing_policy).to receive(:host_lost) + allow(load_balancing_policy).to receive(:setup) + allow(load_balancing_policy).to receive(:teardown) + allow(load_balancing_policy).to receive(:distance) + allow(load_balancing_policy).to receive(:plan) + end [:coordinator, :duration, :parameters, :request, :started_at, :client].each do |method| describe("##{method}") do @@ -41,12 +54,12 @@ module Execution let(:future_rows) { Future::Value.new(rows) } it "loads #{method} from system_traces.sessions" do - expect(client).to receive(:query).once.with(statement, VOID_OPTIONS).and_return(future_rows) + expect(client).to receive(:query).once.with(statement, execution_options).and_return(future_rows) expect(trace.__send__(method)).to eq(data[method.to_s]) end it "loads #{method} only once" do - expect(client).to receive(:query).once.with(statement, VOID_OPTIONS).and_return(future_rows) + expect(client).to receive(:query).once.with(statement, execution_options).and_return(future_rows) 10.times { expect(trace.__send__(method)).to eq(data[method.to_s]) } end end @@ -70,12 +83,12 @@ module Execution let(:future_rows) { Future::Value.new(rows) } it "loads events from system_traces.events" do - expect(client).to receive(:query).once.with(statement, VOID_OPTIONS).and_return(future_rows) + expect(client).to receive(:query).once.with(statement, execution_options).and_return(future_rows) expect(trace.events).to have(5).events end it "loads events only once" do - expect(client).to receive(:query).once.with(statement, VOID_OPTIONS).and_return(future_rows) + expect(client).to receive(:query).once.with(statement, execution_options).and_return(future_rows) 10.times { expect(trace.events).to have(5).events } end end diff --git a/spec/cassandra/executors/same_thread_spec.rb b/spec/cassandra/executors/same_thread_spec.rb index f656b22a9..84cdcedba 100644 --- a/spec/cassandra/executors/same_thread_spec.rb +++ b/spec/cassandra/executors/same_thread_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/executors/thread_pool_spec.rb b/spec/cassandra/executors/thread_pool_spec.rb index 11aa7322b..021b49d1c 100644 --- a/spec/cassandra/executors/thread_pool_spec.rb +++ b/spec/cassandra/executors/thread_pool_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/future_spec.rb b/spec/cassandra/future_spec.rb index 781909839..38e4b3841 100644 --- a/spec/cassandra/future_spec.rb +++ b/spec/cassandra/future_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/index_spec.rb b/spec/cassandra/index_spec.rb new file mode 100644 index 000000000..fe46a3b4b --- /dev/null +++ b/spec/cassandra/index_spec.rb @@ -0,0 +1,72 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +module Cassandra + describe(Index) do + let(:table) { double('table') } + it 'should unescape target' do + ind = Cassandra::Index.new(table, 'myind', :composites, 'f1', {}) + expect(ind.target).to eq('f1') + + ind = Cassandra::Index.new(table, 'myind', :composites, '"f1"', {}) + expect(ind.target).to eq('f1') + end + + context :to_cql do + let(:ks) { double('keyspace') } + let(:col) { double('col') } + let(:col2) { double('col2') } + let(:col3) { double('from') } + let(:options) { {'target' => 'f1'} } + let(:id) { 1234 } + let(:where) { 'col=7' } + + before do + allow(ks).to receive(:name).and_return('myks1') + allow(table).to receive(:name).and_return('table1') + allow(table).to receive(:keyspace).and_return(ks) + end + + it 'should quote keyspace, view name, table name, columns properly for regular index' do + t = Cassandra::Index.new(table, 'myind1', :composites, 'f1', options) + expect(t.to_cql).to eq('CREATE INDEX "myind1" ON "myks1"."table1" ("f1");') + end + + it 'should quote keyspace, view name, table name, columns properly for custom index' do + options['opt1'] = 'val1' + options['class_name'] = 'com.datastax.Custom' + t = Cassandra::Index.new(table, 'myind1', :custom, 'f1', options) + expected = <<-EOF +CREATE CUSTOM INDEX "myind1" ON "myks1"."table1" ("f1") USING 'com.datastax.Custom' WITH OPTIONS = {'opt1': 'val1'}; +EOF + expect(t.to_cql).to eq(expected.chomp) + end + + it 'should exclude "WITH OPTIONS" if there are no options' do + options['class_name'] = 'com.datastax.Custom' + t = Cassandra::Index.new(table, 'myind1', :custom, 'f1', options) + expected = <<-EOF +CREATE CUSTOM INDEX "myind1" ON "myks1"."table1" ("f1") USING 'com.datastax.Custom'; + EOF + expect(t.to_cql).to eq(expected.chomp) + end + end + end +end diff --git a/spec/cassandra/keyspace_spec.rb b/spec/cassandra/keyspace_spec.rb new file mode 100644 index 000000000..ebab00423 --- /dev/null +++ b/spec/cassandra/keyspace_spec.rb @@ -0,0 +1,162 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +include Cassandra::Types +module Cassandra + describe(Keyspace) do + let(:view) { double('view') } + let(:view2) { double('view2') } + let(:table) { double('table') } + let(:table2) { double('table2') } + let(:index1) { double('index') } + let(:index2) { double('index2') } + + context :view do + let(:ks) { Keyspace.new('myks', true, nil, { 'mytable' => table, 'tbl2' => table2 }, + nil, nil, nil, { 'myview' => view, 'view2' => view2 }) + } + + before do + allow(view).to receive(:set_keyspace) + allow(view2).to receive(:set_keyspace) + allow(view2).to receive(:base_table) + allow(table).to receive(:set_keyspace) + allow(table2).to receive(:set_keyspace) + allow(table).to receive(:each_index) + allow(table2).to receive(:each_index) + end + + context :has_materialized_view? do + it 'should return true if the view exists and has a base-table' do + expect(view).to receive(:base_table).and_return(table).exactly(2).times + expect(table).to receive(:add_view).with(view) + expect(ks.has_materialized_view?('myview')).to be_truthy + end + + it 'should return false if the view exists but does not have a base-table' do + expect(view).to receive(:base_table).and_return(nil).exactly(2).times + expect(ks.has_materialized_view?('myview')).to be_falsey + end + end + + context :materialized_view do + it 'should return the view if it exists and has a base-table' do + expect(view).to receive(:base_table).and_return(table).exactly(2).times + expect(table).to receive(:add_view).with(view) + expect(ks.materialized_view('myview')).to be(view) + end + + it 'should return nil if the view exists but does not have a base-table' do + expect(view).to receive(:base_table).and_return(nil).exactly(2).times + expect(ks.materialized_view('myview')).to be_nil + end + end + + context :materialized_views do + it 'should return the view if it exists and has a base-table' do + expect(view).to receive(:base_table).and_return(table).exactly(2).times + expect(table).to receive(:add_view).with(view) + expect(ks.materialized_views).to eq([view]) + end + + it 'should not return view if the view exists but does not have a base-table' do + expect(view).to receive(:base_table).and_return(nil).exactly(2).times + expect(ks.materialized_views).to be_empty + end + end + + context :each_materialized_view do + it 'should return the view if it exists and has a base-table' do + expect(view).to receive(:base_table).and_return(table).exactly(2).times + expect(table).to receive(:add_view).with(view) + result = [] + ks.each_materialized_view do |v| + result << v + end + + expect(result).to eq([view]) + end + + it 'should not return view if the view exists but does not have a base-table' do + expect(view).to receive(:base_table).and_return(nil).exactly(2).times + result = [] + ks.each_materialized_view do |v| + result << v + end + + expect(result).to be_empty + end + end + end + + context :index do + let(:ks) { Keyspace.new('myks', true, nil, { 'mytable' => table, 'tbl2' => table2 }, nil, nil, nil, {}) } + + before do + allow(table).to receive(:set_keyspace) + allow(table2).to receive(:set_keyspace) + allow(table2).to receive(:each_index) do |&block| + block.call(index1) + block.call(index2) + end + allow(table).to receive(:each_index) + allow(index1).to receive(:name).and_return("index1") + allow(index2).to receive(:name).and_return("index2") + end + + context :has_index? do + it 'should return true if the index exists' do + expect(ks.has_index?('index1')).to be_truthy + end + + it 'should return false if the index does not exist' do + expect(ks.has_index?('myindex')).to be_falsey + end + end + + context :index do + it 'should return the index if it exists' do + expect(ks.index('index1')).to be(index1) + end + + it 'should return nil if the index does not exist' do + expect(ks.index('myindex')).to be_nil + end + end + + context :indexes do + it 'should return the indexes' do + expect(ks.indexes).to eq([index1, index2]) + end + end + + context :each_index do + it 'should iterate the indexes' do + result = [] + ks.each_index do |v| + result << v + end + + expect(result).to eq([index1, index2]) + end + end + end + end +end diff --git a/spec/cassandra/load_balancing/policies/dc_aware_round_robin_spec.rb b/spec/cassandra/load_balancing/policies/dc_aware_round_robin_spec.rb index 6962f48e9..0e297f13b 100644 --- a/spec/cassandra/load_balancing/policies/dc_aware_round_robin_spec.rb +++ b/spec/cassandra/load_balancing/policies/dc_aware_round_robin_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ module LoadBalancing module Policies describe(DCAwareRoundRobin) do let(:datacenter) { 'DC1' } - let(:max_remote_hosts_to_use) { nil } + let(:max_remote_hosts_to_use) { 0 } let(:use_remote_hosts_for_local_consistency) { false } let(:policy) { DCAwareRoundRobin.new(datacenter, max_remote_hosts_to_use, use_remote_hosts_for_local_consistency) } @@ -34,6 +34,15 @@ module Policies let(:distance) { policy.distance(host) } + describe '#constructor' do + let(:use_remote_hosts_for_local_consistency) { true } + it 'should error out if use_remmote_hosts_for_local_consistency is true and max_remote_hosts_to_use is 0' do + expect { + policy + }.to raise_error(ArgumentError) + end + end + describe('#host_up') do before do policy.host_up(host) @@ -49,6 +58,7 @@ module Policies end context('and another host in remote datacenter is up') do + let(:max_remote_hosts_to_use) { nil } let(:another_host) { Host.new('127.0.0.2', nil, nil, host_datacenter) } before do @@ -180,7 +190,12 @@ module Policies let(:keyspace) { 'foo' } let(:statement) { VOID_STATEMENT } let(:consistency) { :one } - let(:options) { Execution::Options.new({:consistency => consistency}) } + let(:retry_policy) { double('retry_policy') } + let(:options) { + Execution::Options.new({ consistency: consistency, + load_balancing_policy: policy, + retry_policy: retry_policy}) + } let(:plan) { policy.plan(keyspace, statement, options) } @@ -202,6 +217,10 @@ module Policies policy.host_up(host) remote_hosts.each {|host| policy.host_up(host)} local_hosts.each {|host| policy.host_up(host)} + + allow(retry_policy).to receive(:read_timeout) + allow(retry_policy).to receive(:write_timeout) + allow(retry_policy).to receive(:unavailable) end it 'prioritizes hosts first' do diff --git a/spec/cassandra/load_balancing/policies/round_robin_spec.rb b/spec/cassandra/load_balancing/policies/round_robin_spec.rb index c314c5d93..557336228 100644 --- a/spec/cassandra/load_balancing/policies/round_robin_spec.rb +++ b/spec/cassandra/load_balancing/policies/round_robin_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/load_balancing/policies/token_aware_spec.rb b/spec/cassandra/load_balancing/policies/token_aware_spec.rb index 578502d8c..c37cdb882 100644 --- a/spec/cassandra/load_balancing/policies/token_aware_spec.rb +++ b/spec/cassandra/load_balancing/policies/token_aware_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/load_balancing/policies/white_list_spec.rb b/spec/cassandra/load_balancing/policies/white_list_spec.rb index 960053196..92e936c60 100644 --- a/spec/cassandra/load_balancing/policies/white_list_spec.rb +++ b/spec/cassandra/load_balancing/policies/white_list_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/materialized_view_spec.rb b/spec/cassandra/materialized_view_spec.rb new file mode 100644 index 000000000..206889cb4 --- /dev/null +++ b/spec/cassandra/materialized_view_spec.rb @@ -0,0 +1,112 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +include Cassandra::Types +module Cassandra + + describe(MaterializedView) do + context :to_cql do + let(:ks) { double('keyspace')} + let(:table) { double('table')} + let(:col) { double('col')} + let(:col2) { double('col2')} + let(:col3) { double('from')} + let(:options) { double('options')} + let(:id) { 1234 } + let(:where) { 'col=7'} + + before do + allow(ks).to receive(:name).and_return('myks1') + allow(ks).to receive(:table).with('table1').and_return(table) + allow(table).to receive(:name).and_return('table1') + allow(col).to receive(:name).and_return('col') + allow(col).to receive(:type).and_return(Cassandra::Types.int) + allow(col2).to receive(:name).and_return('col2') + allow(col2).to receive(:type).and_return(int) + allow(col3).to receive(:name).and_return('from') + allow(col3).to receive(:type).and_return(varchar) + allow(options).to receive(:to_cql).and_return('opt1=value1') + end + + it 'should quote keyspace, view name, table name, columns properly' do + t = MaterializedView.new(ks, 'myview1', [col], [], [col2, col3], options, false, where, 'table1', id) + expected_cql = <<-EOF +CREATE MATERIALIZED VIEW "myks1"."myview1" AS +SELECT col, "col2", "from" +FROM "myks1"."table1" +WHERE col=7 +PRIMARY KEY ((col)) +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should quote primary key properly for simple partition key' do + t = MaterializedView.new(ks, 'myview1', [col], [col2], [col3], options, false, where, 'table1', id) + expected_cql = <<-EOF +CREATE MATERIALIZED VIEW "myks1"."myview1" AS +SELECT col, "col2", "from" +FROM "myks1"."table1" +WHERE col=7 +PRIMARY KEY ((col), "col2") +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should quote primary key properly for composite partition key' do + t = MaterializedView.new(ks, 'myview1', [col, col2], [col3], [], options, false, where, 'table1', id) + expected_cql = <<-EOF +CREATE MATERIALIZED VIEW "myks1"."myview1" AS +SELECT col, "col2", "from" +FROM "myks1"."table1" +WHERE col=7 +PRIMARY KEY ((col, "col2"), "from") +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should handle no where-clause properly' do + t = MaterializedView.new(ks, 'myview1', [col, col2], [col3], [], options, false, nil, 'table1', id) + expected_cql = <<-EOF +CREATE MATERIALIZED VIEW "myks1"."myview1" AS +SELECT col, "col2", "from" +FROM "myks1"."table1" +PRIMARY KEY ((col, "col2"), "from") +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should handle include-all-columns properly' do + t = MaterializedView.new(ks, 'myview1', [col, col2], [col3], [], options, true, nil, 'table1', id) + expected_cql = <<-EOF +CREATE MATERIALIZED VIEW "myks1"."myview1" AS +SELECT * +FROM "myks1"."table1" +PRIMARY KEY ((col, "col2"), "from") +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + end + end +end diff --git a/spec/cassandra/murmur3_spec.rb b/spec/cassandra/murmur3_spec.rb index f12e578ee..767979be8 100644 --- a/spec/cassandra/murmur3_spec.rb +++ b/spec/cassandra/murmur3_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/promise/signal_spec.rb b/spec/cassandra/promise/signal_spec.rb index eb05c7a40..981447270 100644 --- a/spec/cassandra/promise/signal_spec.rb +++ b/spec/cassandra/promise/signal_spec.rb @@ -129,7 +129,7 @@ class Promise signal Thread.new { resolved = true; sleep(0.02); signal.success(value) } expect { signal.get(0) }.to raise_error(Errors::TimeoutError) - expect(signal.get(0.03)).to eq(value) + expect(signal.get(0.05)).to eq(value) expect(resolved).to be_truthy end end diff --git a/spec/cassandra/protocol/coder_spec.rb b/spec/cassandra/protocol/coder_spec.rb index a7b6b4b16..37408adbe 100644 --- a/spec/cassandra/protocol/coder_spec.rb +++ b/spec/cassandra/protocol/coder_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/cql_byte_buffer_spec.rb b/spec/cassandra/protocol/cql_byte_buffer_spec.rb index 676ba2f8c..4e3c013c4 100644 --- a/spec/cassandra/protocol/cql_byte_buffer_spec.rb +++ b/spec/cassandra/protocol/cql_byte_buffer_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/cql_protocol_handler_spec.rb b/spec/cassandra/protocol/cql_protocol_handler_spec.rb index 2a8371a6f..3337071b3 100644 --- a/spec/cassandra/protocol/cql_protocol_handler_spec.rb +++ b/spec/cassandra/protocol/cql_protocol_handler_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/auth_response_request_spec.rb b/spec/cassandra/protocol/requests/auth_response_request_spec.rb index b261eeb99..135c3dff9 100644 --- a/spec/cassandra/protocol/requests/auth_response_request_spec.rb +++ b/spec/cassandra/protocol/requests/auth_response_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/batch_request_spec.rb b/spec/cassandra/protocol/requests/batch_request_spec.rb index fd63e683c..49211e738 100644 --- a/spec/cassandra/protocol/requests/batch_request_spec.rb +++ b/spec/cassandra/protocol/requests/batch_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/credentials_request_spec.rb b/spec/cassandra/protocol/requests/credentials_request_spec.rb index 7884622e8..db8410538 100644 --- a/spec/cassandra/protocol/requests/credentials_request_spec.rb +++ b/spec/cassandra/protocol/requests/credentials_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/execute_request_spec.rb b/spec/cassandra/protocol/requests/execute_request_spec.rb index ac8b14f06..3f326d409 100644 --- a/spec/cassandra/protocol/requests/execute_request_spec.rb +++ b/spec/cassandra/protocol/requests/execute_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/options_request_spec.rb b/spec/cassandra/protocol/requests/options_request_spec.rb index ef3830f89..ad10aa013 100644 --- a/spec/cassandra/protocol/requests/options_request_spec.rb +++ b/spec/cassandra/protocol/requests/options_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/prepare_request_spec.rb b/spec/cassandra/protocol/requests/prepare_request_spec.rb index 4dc19aa04..83c30a8f1 100644 --- a/spec/cassandra/protocol/requests/prepare_request_spec.rb +++ b/spec/cassandra/protocol/requests/prepare_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/query_request_spec.rb b/spec/cassandra/protocol/requests/query_request_spec.rb index dc04e803b..31f2ff707 100644 --- a/spec/cassandra/protocol/requests/query_request_spec.rb +++ b/spec/cassandra/protocol/requests/query_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/register_request_spec.rb b/spec/cassandra/protocol/requests/register_request_spec.rb index 16c4be3a8..f08427cde 100644 --- a/spec/cassandra/protocol/requests/register_request_spec.rb +++ b/spec/cassandra/protocol/requests/register_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/requests/startup_request_spec.rb b/spec/cassandra/protocol/requests/startup_request_spec.rb index 3dbf70960..bbb0bd184 100644 --- a/spec/cassandra/protocol/requests/startup_request_spec.rb +++ b/spec/cassandra/protocol/requests/startup_request_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/auth_challenge_response_spec.rb b/spec/cassandra/protocol/responses/auth_challenge_response_spec.rb index dafc1672e..fde4122a7 100644 --- a/spec/cassandra/protocol/responses/auth_challenge_response_spec.rb +++ b/spec/cassandra/protocol/responses/auth_challenge_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/auth_success_response_spec.rb b/spec/cassandra/protocol/responses/auth_success_response_spec.rb index beddee03f..694a2d4c2 100644 --- a/spec/cassandra/protocol/responses/auth_success_response_spec.rb +++ b/spec/cassandra/protocol/responses/auth_success_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/authenticate_response_spec.rb b/spec/cassandra/protocol/responses/authenticate_response_spec.rb index e0e2ae4db..bf10c1f7f 100644 --- a/spec/cassandra/protocol/responses/authenticate_response_spec.rb +++ b/spec/cassandra/protocol/responses/authenticate_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/error_response_spec.rb b/spec/cassandra/protocol/responses/error_response_spec.rb index 0e9704b0b..c81364055 100644 --- a/spec/cassandra/protocol/responses/error_response_spec.rb +++ b/spec/cassandra/protocol/responses/error_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/prepared_result_response_spec.rb b/spec/cassandra/protocol/responses/prepared_result_response_spec.rb index f4d5ea369..85b8b7a7a 100644 --- a/spec/cassandra/protocol/responses/prepared_result_response_spec.rb +++ b/spec/cassandra/protocol/responses/prepared_result_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/raw_rows_result_response_spec.rb b/spec/cassandra/protocol/responses/raw_rows_result_response_spec.rb index a8f8a03dd..6f074dab1 100644 --- a/spec/cassandra/protocol/responses/raw_rows_result_response_spec.rb +++ b/spec/cassandra/protocol/responses/raw_rows_result_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/ready_response_spec.rb b/spec/cassandra/protocol/responses/ready_response_spec.rb index 43711558b..2e974bd20 100644 --- a/spec/cassandra/protocol/responses/ready_response_spec.rb +++ b/spec/cassandra/protocol/responses/ready_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/rows_result_response_spec.rb b/spec/cassandra/protocol/responses/rows_result_response_spec.rb index d220cc466..e0e95c1f9 100644 --- a/spec/cassandra/protocol/responses/rows_result_response_spec.rb +++ b/spec/cassandra/protocol/responses/rows_result_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/schema_change_event_response_spec.rb b/spec/cassandra/protocol/responses/schema_change_event_response_spec.rb index 65c49a943..c189a0d2c 100644 --- a/spec/cassandra/protocol/responses/schema_change_event_response_spec.rb +++ b/spec/cassandra/protocol/responses/schema_change_event_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/schema_change_result_response_spec.rb b/spec/cassandra/protocol/responses/schema_change_result_response_spec.rb index 90b6e5240..7e0b8a368 100644 --- a/spec/cassandra/protocol/responses/schema_change_result_response_spec.rb +++ b/spec/cassandra/protocol/responses/schema_change_result_response_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/set_keyspace_result_response_spec.rb b/spec/cassandra/protocol/responses/set_keyspace_result_response_spec.rb index c4bcc0c34..bb60f3c8a 100644 --- a/spec/cassandra/protocol/responses/set_keyspace_result_response_spec.rb +++ b/spec/cassandra/protocol/responses/set_keyspace_result_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/status_change_event_response_spec.rb b/spec/cassandra/protocol/responses/status_change_event_response_spec.rb index b47e6d6a9..aff24c6e3 100644 --- a/spec/cassandra/protocol/responses/status_change_event_response_spec.rb +++ b/spec/cassandra/protocol/responses/status_change_event_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/supported_response_spec.rb b/spec/cassandra/protocol/responses/supported_response_spec.rb index fd961363a..691646112 100644 --- a/spec/cassandra/protocol/responses/supported_response_spec.rb +++ b/spec/cassandra/protocol/responses/supported_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/topology_change_event_response_spec.rb b/spec/cassandra/protocol/responses/topology_change_event_response_spec.rb index cafb48e33..e55532d59 100644 --- a/spec/cassandra/protocol/responses/topology_change_event_response_spec.rb +++ b/spec/cassandra/protocol/responses/topology_change_event_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/responses/void_result_response_spec.rb b/spec/cassandra/protocol/responses/void_result_response_spec.rb index 4ab5a73f1..e3b780b2b 100644 --- a/spec/cassandra/protocol/responses/void_result_response_spec.rb +++ b/spec/cassandra/protocol/responses/void_result_response_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/v1/decoder_spec.rb b/spec/cassandra/protocol/v1/decoder_spec.rb index 02a03e7b6..f685b8cfe 100644 --- a/spec/cassandra/protocol/v1/decoder_spec.rb +++ b/spec/cassandra/protocol/v1/decoder_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/v3/decoder_spec.rb b/spec/cassandra/protocol/v3/decoder_spec.rb index eb717ee5e..c5c362a56 100644 --- a/spec/cassandra/protocol/v3/decoder_spec.rb +++ b/spec/cassandra/protocol/v3/decoder_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/protocol/v4/decoder_spec.rb b/spec/cassandra/protocol/v4/decoder_spec.rb index 5957a15f1..b8ca1f6d3 100644 --- a/spec/cassandra/protocol/v4/decoder_spec.rb +++ b/spec/cassandra/protocol/v4/decoder_spec.rb @@ -1,7 +1,7 @@ # encoding: ascii-8bit #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/retry/policies/default_spec.rb b/spec/cassandra/retry/policies/default_spec.rb index d7a423a04..0422593e6 100644 --- a/spec/cassandra/retry/policies/default_spec.rb +++ b/spec/cassandra/retry/policies/default_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/retry/policies/downgrading_consistency_spec.rb b/spec/cassandra/retry/policies/downgrading_consistency_spec.rb index ab092ee65..79294d129 100644 --- a/spec/cassandra/retry/policies/downgrading_consistency_spec.rb +++ b/spec/cassandra/retry/policies/downgrading_consistency_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/retry/policies/fallthrough_spec.rb b/spec/cassandra/retry/policies/fallthrough_spec.rb index 0fb984431..785d66b36 100644 --- a/spec/cassandra/retry/policies/fallthrough_spec.rb +++ b/spec/cassandra/retry/policies/fallthrough_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/session_spec.rb b/spec/cassandra/session_spec.rb index f9e119e59..2cc0cb2bf 100644 --- a/spec/cassandra/session_spec.rb +++ b/spec/cassandra/session_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,12 +20,54 @@ module Cassandra describe(Session) do - let(:default_options) { {:consistency => :one, :timeout => 5, :trace => false} } + let(:load_balancing_policy) { double('load_balancing_policy')} + let(:retry_policy) { double('retry_policy')} + let(:default_options) { + { + consistency: :one, + timeout: 5, + trace: false, + load_balancing_policy: load_balancing_policy, + retry_policy: retry_policy + } + } + let(:profile_manager) { double('profile_manager') } + let(:profile) { + Execution::Profile.new(load_balancing_policy: load_balancing_policy, + retry_policy: retry_policy, + consistency: :one, + timeout: 12) + } let(:session_options) { Execution::Options.new(default_options) } let(:client) { double('cassandra-driver') } - let(:session) { Session.new(client, session_options, Future::Factory.new(Executors::SameThread.new)) } + let(:session) { + Session.new(client, session_options, Future::Factory.new(Executors::SameThread.new), profile_manager) + } + + before do + allow(load_balancing_policy).to receive(:host_up) + allow(load_balancing_policy).to receive(:host_down) + allow(load_balancing_policy).to receive(:host_found) + allow(load_balancing_policy).to receive(:host_lost) + allow(load_balancing_policy).to receive(:setup) + allow(load_balancing_policy).to receive(:teardown) + allow(load_balancing_policy).to receive(:distance) + allow(load_balancing_policy).to receive(:plan) + + allow(retry_policy).to receive(:read_timeout) + allow(retry_policy).to receive(:write_timeout) + allow(retry_policy).to receive(:unavailable) + + allow(profile_manager).to receive(:profiles).and_return({good_profile: profile}) + end describe('#execute_async') do + it 'should error out if a bad profile name is provided' do + expect { + session.execute_async('SELECT * FROM songs', execution_profile: :bad_profile).get + }.to raise_error(ArgumentError) + end + context 'cql string' do context 'without arguments' do let(:cql) { 'SELECT * FROM songs' } @@ -35,7 +77,7 @@ module Cassandra statement = double('simple statement') expect(Statements::Simple).to receive(:new).once.with(cql, EMPTY_LIST, EMPTY_LIST, false).and_return(statement) - expect(client).to receive(:query).once.with(statement, session_options).and_return(promise) + expect(statement).to receive(:accept).once.with(client, session_options).and_return(promise) expect(session.execute_async(cql)).to eq(promise) end end @@ -48,7 +90,7 @@ module Cassandra statement = double('simple statement') expect(Statements::Simple).to receive(:new).once.with(cql, [1], [], false).and_return(statement) - expect(client).to receive(:query).once.with(statement, session_options.override(arguments: [1])).and_return(promise) + expect(statement).to receive(:accept).once.with(client, session_options.override(arguments: [1])).and_return(promise) expect(session.execute_async(cql, arguments: [1])).to eq(promise) end end @@ -61,7 +103,7 @@ module Cassandra statement = double('simple statement') expect(Statements::Simple).to receive(:new).once.with(cql, [1], [:int], false).and_return(statement) - expect(client).to receive(:query).once.with(statement, session_options.override(arguments: [1], type_hints: [:int])).and_return(promise) + expect(statement).to receive(:accept).once.with(client, session_options.override(arguments: [1], type_hints: [:int])).and_return(promise) expect(session.execute_async(cql, arguments: [1], type_hints: [:int])).to eq(promise) end end @@ -75,9 +117,18 @@ module Cassandra statement = double('simple statement') expect(Statements::Simple).to receive(:new).once.with(cql, EMPTY_LIST, EMPTY_LIST, false).and_return(statement) - expect(client).to receive(:query).once.with(statement, session_options.override(options)).and_return(promise) + expect(statement).to receive(:accept).once.with(client, session_options.override(options)).and_return(promise) expect(session.execute_async(cql, options)).to eq(promise) end + + it 'should handle execution profile' do + promise = double('promise') + statement = double('simple statement') + + expect(Statements::Simple).to receive(:new).once.with(cql, EMPTY_LIST, EMPTY_LIST, false).and_return(statement) + expect(statement).to receive(:accept).once.with(client, session_options.override(profile)).and_return(promise) + expect(session.execute_async(cql, {execution_profile: :good_profile})).to eq(promise) + end end end @@ -85,14 +136,14 @@ module Cassandra let(:cql) { "INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)" } let(:result_metadata) { nil } let(:params_metadata) { Array.new(5) } - let(:statement) { Statements::Prepared.new(nil, nil, cql, params_metadata, result_metadata, nil, nil, nil, nil, VOID_OPTIONS, nil, nil, nil, nil, nil) } + let(:statement) { Statements::Prepared.new(nil, nil, nil, cql, params_metadata, result_metadata, nil, nil, nil, nil, VOID_OPTIONS, nil, nil, nil, nil, nil) } it 'binds and executes result' do promise = double('promise') bound_statement = double('bound statement') options = double('options') - expect(session_options).to receive(:override).once.with(arguments: [1, 2, 3, 4, 5]).and_return(options) + expect(session_options).to receive(:override).once.with(nil, arguments: [1, 2, 3, 4, 5]).and_return(options) allow(options).to receive(:arguments).and_return([1, 2, 3, 4, 5]) expect(statement).to receive(:bind).with([1, 2, 3, 4, 5]).and_return(bound_statement) expect(client).to receive(:execute).once.with(bound_statement, options).and_return(promise) @@ -105,7 +156,7 @@ module Cassandra let(:result_metadata) { nil } let(:params_metadata) { Array.new(5) } let(:params) { [1,2,3,4,5] } - let(:statement) { Statements::Bound.new(cql, params_metadata, result_metadata, params) } + let(:statement) { Statements::Bound.new(nil, cql, params_metadata, result_metadata, params) } it 'executes statement' do promise = double('promise') @@ -131,6 +182,12 @@ module Cassandra describe('#prepare_async') do let(:cql) { 'SELECT * FROM songs' } + it 'should error out if a bad profile name is provided' do + expect { + session.prepare_async('SELECT * FROM songs', execution_profile: :bad_profile).get + }.to raise_error(ArgumentError) + end + context 'without options' do it 'prepares cql with the client' do promise = double('promise') @@ -147,7 +204,16 @@ module Cassandra promise = double('promise') opts = double('options') - expect(session_options).to receive(:override).once.with(options).and_return(opts) + expect(session_options).to receive(:override).once.with(nil, options).and_return(opts) + expect(client).to receive(:prepare).once.with(cql, opts).and_return(promise) + expect(session.prepare_async(cql, options)).to eq(promise) + end + + it 'should handle execution profile' do + promise = double('promise') + opts = double('options') + options = {execution_profile: :good_profile} + expect(session_options).to receive(:override).once.with(profile, options).and_return(opts) expect(client).to receive(:prepare).once.with(cql, opts).and_return(promise) expect(session.prepare_async(cql, options)).to eq(promise) end @@ -210,13 +276,13 @@ module Cassandra describe('#prepare') do let(:promise) { double('promise') } - let(:args) { [double('statement to prepare')] } + let(:statement) { double('statement to prepare') } it "resolves a promise returned by #prepare_async" do - expect(session).to receive(:prepare_async).with(*args).and_return(promise) + expect(session).to receive(:prepare_async).with(statement, nil).and_return(promise) expect(promise).to receive(:get).once - session.prepare(*args) + session.prepare(statement) end end diff --git a/spec/cassandra/table_spec.rb b/spec/cassandra/table_spec.rb new file mode 100644 index 000000000..d71d24442 --- /dev/null +++ b/spec/cassandra/table_spec.rb @@ -0,0 +1,243 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +include Cassandra::Types +module Cassandra + + describe(Table) do + let(:ks) { double('keyspace') } + let(:col) { double('col') } + let(:col2) { double('col2') } + let(:col3) { double('from') } + let(:quote_column) { double('"my"col"') } + let(:options) { double('options') } + let(:id) { 1234 } + before do + allow(ks).to receive(:name).and_return('myks1') + allow(col).to receive(:name).and_return('col') + allow(col).to receive(:type).and_return(Cassandra::Types.int) + allow(col2).to receive(:name).and_return('col2') + allow(col2).to receive(:type).and_return(int) + allow(col3).to receive(:name).and_return('from') + allow(col3).to receive(:type).and_return(varchar) + allow(quote_column).to receive(:name).and_return('"my"col"') + allow(quote_column).to receive(:type).and_return(varchar) + allow(options).to receive(:to_cql).and_return('opt1=value1') + end + + context :index do + let(:index1) { double('index1') } + let(:index2) { double('index2') } + let(:table) { Table.new(ks, 'mytable1', [col], [], [col2, col3, quote_column], options, [], id) } + + before do + allow(index1).to receive(:name).and_return('index1') + allow(index2).to receive(:name).and_return('index2') + table.add_index(index1) + table.add_index(index2) + end + + context :has_index? do + it 'should find an index that exists' do + expect(table.has_index?('index1')).to be_truthy + end + + it 'should fail to find an index that does not exist' do + expect(table.has_index?('index3')).to be_falsey + end + end + + context :index do + it 'should find an index that exists' do + expect(table.index('index1')).to be(index1) + end + + it 'should fail to find an index that does not exist' do + expect(table.index('index3')).to be_nil + end + end + + it 'should return collection of indexes' do + expect(table.indexes).to eq([index1, index2]) + end + + it 'should support iterating indexes' do + result = [] + table.each_index do |index| + result << index + end + + expect(result).to eq([index1, index2]) + end + end + + context :materialized_view do + let(:view1) { double('view1') } + let(:view2) { double('view2') } + let(:table) { Table.new(ks, 'mytable1', [col], [], [col2, col3, quote_column], options, [], id) } + + before do + allow(view1).to receive(:name).and_return('view1') + allow(view2).to receive(:name).and_return('view2') + table.add_view(view1) + table.add_view(view2) + end + + context :has_materialized_view? do + it 'should find an materialized_view that exists' do + expect(table.has_materialized_view?('view1')).to be_truthy + end + + it 'should fail to find an materialized_view that does not exist' do + expect(table.has_materialized_view?('view3')).to be_falsey + end + end + + context :materialized_view do + it 'should find an materialized_view that exists' do + expect(table.materialized_view('view1')).to be(view1) + end + + it 'should fail to find an materialized_view that does not exist' do + expect(table.materialized_view('view3')).to be_nil + end + end + + it 'should return collection of materialized_views' do + expect(table.materialized_views).to eq([view1, view2]) + end + + it 'should support iterating materialized_views' do + result = [] + table.each_materialized_view do |materialized_view| + result << materialized_view + end + + expect(result).to eq([view1, view2]) + end + end + + context :trigger do + let(:trigger1) { double('trigger1') } + let(:trigger2) { double('trigger2') } + let(:table) { Table.new(ks, 'mytable1', [col], [], [col2, col3, quote_column], options, [], id) } + + before do + allow(trigger1).to receive(:name).and_return('trigger1') + allow(trigger2).to receive(:name).and_return('trigger2') + table.add_trigger(trigger1) + table.add_trigger(trigger2) + end + + context :has_trigger? do + it 'should find an trigger that exists' do + expect(table.has_trigger?('trigger1')).to be_truthy + end + + it 'should fail to find an trigger that does not exist' do + expect(table.has_trigger?('trigger3')).to be_falsey + end + end + + context :trigger do + it 'should find an trigger that exists' do + expect(table.trigger('trigger1')).to be(trigger1) + end + + it 'should fail to find an trigger that does not exist' do + expect(table.trigger('trigger3')).to be_nil + end + end + + it 'should return collection of triggers' do + expect(table.triggers).to eq([trigger1, trigger2]) + end + + it 'should support iterating triggers' do + result = [] + table.each_trigger do |trigger| + result << trigger + end + + expect(result).to eq([trigger1, trigger2]) + end + end + + context :to_cql do + + it 'should quote keyspace, table, columns properly' do + t = Table.new(ks, 'mytable1', [col], [], [col2, col3, quote_column], options, [], id) + expected_cql = <<-EOF +CREATE TABLE "myks1"."mytable1" ( + col int PRIMARY KEY, + "col2" int, + "from" text, + """my""col""" text +) +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should quote primary key properly for simple partition key' do + t = Table.new(ks, 'mytable1', [col], [col2], [col3], options, [], id) + expected_cql = <<-EOF +CREATE TABLE "myks1"."mytable1" ( + col int, + "col2" int, + "from" text, + PRIMARY KEY (col, "col2") +) +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should quote primary key properly for composite partition key' do + t = Table.new(ks, 'mytable1', [col, col2], [col3], [], options, [], id) + expected_cql = <<-EOF +CREATE TABLE "myks1"."mytable1" ( + col int, + "col2" int, + "from" text, + PRIMARY KEY ((col, "col2"), "from") +) +WITH opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + + it 'should handle clustering order properly' do + t = Table.new(ks, 'mytable1', [col, col2], [col3], [], options, [:desc], id) + expected_cql = <<-EOF +CREATE TABLE "myks1"."mytable1" ( + col int, + "col2" int, + "from" text, + PRIMARY KEY ((col, "col2"), "from") +) +WITH CLUSTERING ORDER BY ("from" DESC) + AND opt1=value1; + EOF + expect(t.to_cql).to eq(expected_cql.chomp) + end + end + end +end diff --git a/spec/cassandra/time_uuid_spec.rb b/spec/cassandra/time_uuid_spec.rb index 3aa1be2e1..bb85fb717 100644 --- a/spec/cassandra/time_uuid_spec.rb +++ b/spec/cassandra/time_uuid_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/util_spec.rb b/spec/cassandra/util_spec.rb new file mode 100644 index 000000000..fe344a7b0 --- /dev/null +++ b/spec/cassandra/util_spec.rb @@ -0,0 +1,48 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +module Cassandra + describe Util do + describe '.guess_type' do + specs = [ + [Types.text, 'test'], + [Types.varint, 2**64], + [Types.bigint, 2**63], + [Types.boolean, true], + [Types.boolean, false], + [Types.decimal, ::BigDecimal.new('1042342234234.123423435647768234')], + [Types.double, 10000.123123123], + [Types.inet, ::IPAddr.new('8.8.8.8')], + [Types.uuid, Uuid.new(::SecureRandom.hex)], + [Types.uuid, TimeUuid.new(::SecureRandom.hex)], + [Types.timestamp, ::Time.now], + [Types.map(Types.text, Types.bigint), { 'text' => 0 }], + [Types.list(Types.varint), [2**64]], + [Types.set(Types.bigint), Set.new([0])], + ] + + specs.each do |expected_type, value| + it "returns #{expected_type} for #{value}" do + expect(Util.guess_type(value)).to eq(expected_type) + end + end + end + end +end diff --git a/spec/cassandra/uuid/generator_spec.rb b/spec/cassandra/uuid/generator_spec.rb index 2facb088e..465303c9c 100644 --- a/spec/cassandra/uuid/generator_spec.rb +++ b/spec/cassandra/uuid/generator_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra/uuid_spec.rb b/spec/cassandra/uuid_spec.rb index cf2af8c27..8a998a684 100644 --- a/spec/cassandra/uuid_spec.rb +++ b/spec/cassandra/uuid_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/cassandra_spec.rb b/spec/cassandra_spec.rb index bcd5ad1a2..856260ae2 100644 --- a/spec/cassandra_spec.rb +++ b/spec/cassandra_spec.rb @@ -8,17 +8,89 @@ # noinspection RubyClassModuleNamingConvention module C class << self - # noinspection RubyResolve - alias :validate :validate_and_massage_options + def validate(*args) + # Actually, we only care about option-validation, and the method returns options + hosts. + validate_and_massage_options(*args).first + end + end +end + +class GoodLBP + def host_up + end + + def host_down + end + + def host_found + end + + def host_lost + end + + def setup + end + + def teardown + end + + def distance + end + + def plan + end +end + +class GoodRetryPolicy + def read_timeout + end + + def write_timeout + end + + def unavailable end end describe Cassandra do context :validate_and_massage_options do + let(:profiles) { + { + p1: Cassandra::Execution::Profile.new(load_balancing_policy: lbp), + p2: Cassandra::Execution::Profile.new(timeout: 0.00005) + } + } + + let(:lbp) { GoodLBP.new } + it 'should ignore spurious options' do expect(C.validate(foo: 1, timeout: 5)).to eq({ timeout: 5 }) end + context 'for execution profiles' do + it 'should validate :execution_profiles is a hash' do + expect(C.validate(execution_profiles: profiles)).to eq({ execution_profiles: profiles }) + expect { C.validate(execution_profiles: []) }.to raise_error(ArgumentError) + end + + it 'should validate that execution profiles and load_balancing_policy are mutually exclusive' do + expect { C.validate(execution_profiles: profiles, load_balancing_policy: lbp) }.to raise_error(ArgumentError) + end + + it 'should validate that execution profiles and consistency are mutually exclusive' do + expect { C.validate(execution_profiles: profiles, consistency: :one) }.to raise_error(ArgumentError) + end + + it 'should validate that execution profiles and timeout are mutually exclusive' do + expect { C.validate(execution_profiles: profiles, timeout: 7) }.to raise_error(ArgumentError) + end + + it 'should validate that execution profiles and retry_policy are mutually exclusive' do + expect { C.validate(execution_profiles: profiles, retry_policy: GoodRetryPolicy.new) }. + to raise_error(ArgumentError) + end + end + context 'for username and password' do it 'should require both or none of username and password' do # None @@ -171,12 +243,12 @@ def decompress end it 'should validate :timeout' do - expect { C.validate(timeout: 'a') }.to raise_error(ArgumentError) - expect { C.validate(timeout: -1) }.to raise_error(ArgumentError) - expect { C.validate(timeout: 0) }.to raise_error(ArgumentError) - expect(C.validate(timeout: 0.5)).to eq({ timeout: 0.5 }) - expect(C.validate(timeout: 38)).to eq({ timeout: 38 }) - expect(C.validate(timeout: nil)).to eq({ timeout: nil }) + ['a', -1, 0].each do |val| + expect { C.validate(timeout: val) }.to raise_error(ArgumentError) + end + [0.5, 38, nil].each do |val| + expect(C.validate(timeout: val)).to eq({ timeout: val }) + end end it 'should validate :heartbeat_interval' do @@ -201,7 +273,7 @@ def decompress expect { C.validate(schema_refresh_delay: 'a') }.to raise_error(ArgumentError) expect { C.validate(schema_refresh_delay: -1) }.to raise_error(ArgumentError) expect { C.validate(schema_refresh_delay: 0) }.to raise_error(ArgumentError) - expect {C.validate(schema_refresh_delay: nil)}.to raise_error(ArgumentError) + expect { C.validate(schema_refresh_delay: nil) }.to raise_error(ArgumentError) expect(C.validate(schema_refresh_delay: 0.5)).to eq({ schema_refresh_delay: 0.5 }) expect(C.validate(schema_refresh_delay: 38)).to eq({ schema_refresh_delay: 38 }) end @@ -210,41 +282,16 @@ def decompress expect { C.validate(schema_refresh_timeout: 'a') }.to raise_error(ArgumentError) expect { C.validate(schema_refresh_timeout: -1) }.to raise_error(ArgumentError) expect { C.validate(schema_refresh_timeout: 0) }.to raise_error(ArgumentError) - expect {C.validate(schema_refresh_timeout: nil)}.to raise_error(ArgumentError) + expect { C.validate(schema_refresh_timeout: nil) }.to raise_error(ArgumentError) expect(C.validate(schema_refresh_timeout: 0.5)).to eq({ schema_refresh_timeout: 0.5 }) expect(C.validate(schema_refresh_timeout: 38)).to eq({ schema_refresh_timeout: 38 }) end it 'should validate :load_balancing_policy option' do - class GoodPolicy - def host_up - end - - def host_down - end - - def host_found - end - - def host_lost - end - - def setup - end - - def teardown - end - - def distance - end - - def plan - end + expect(C.validate(load_balancing_policy: lbp)).to eq({ load_balancing_policy: lbp }) + ['junk', nil].each do |val| + expect { C.validate(load_balancing_policy: val) }.to raise_error(ArgumentError) end - policy = GoodPolicy.new - expect(C.validate(load_balancing_policy: policy)).to eq({ load_balancing_policy: policy }) - expect { C.validate(load_balancing_policy: 'junk') }.to raise_error(ArgumentError) - expect { C.validate(load_balancing_policy: nil) }.to raise_error(ArgumentError) end it 'should validate :reconnection_policy option' do @@ -259,17 +306,7 @@ def schedule end it 'should validate :retry_policy option' do - class GoodPolicy - def read_timeout - end - - def write_timeout - end - - def unavailable - end - end - policy = GoodPolicy.new + policy = GoodRetryPolicy.new expect(C.validate(retry_policy: policy)).to eq({ retry_policy: policy }) expect { C.validate(retry_policy: 'junk') }.to raise_error(ArgumentError) expect { C.validate(retry_policy: nil) }.to raise_error(ArgumentError) @@ -289,21 +326,21 @@ def unavailable end it 'should massage :nodelay to a boolean' do - expect(C.validate(nodelay: nil)).to eq({nodelay: false}) - expect(C.validate(nodelay: 1)).to eq({nodelay: true}) - expect(C.validate(nodelay: 0)).to eq({nodelay: true}) + expect(C.validate(nodelay: nil)).to eq({ nodelay: false }) + expect(C.validate(nodelay: 1)).to eq({ nodelay: true }) + expect(C.validate(nodelay: 0)).to eq({ nodelay: true }) end it 'should massage :trace to a boolean' do - expect(C.validate(trace: nil)).to eq({trace: false}) - expect(C.validate(trace: 1)).to eq({trace: true}) - expect(C.validate(trace: 0)).to eq({trace: true}) + expect(C.validate(trace: nil)).to eq({ trace: false }) + expect(C.validate(trace: 1)).to eq({ trace: true }) + expect(C.validate(trace: 0)).to eq({ trace: true }) end it 'should massage :shuffle_replicas to a boolean' do - expect(C.validate(shuffle_replicas: nil)).to eq({shuffle_replicas: false}) - expect(C.validate(shuffle_replicas: 1)).to eq({shuffle_replicas: true}) - expect(C.validate(shuffle_replicas: 0)).to eq({shuffle_replicas: true}) + expect(C.validate(shuffle_replicas: nil)).to eq({ shuffle_replicas: false }) + expect(C.validate(shuffle_replicas: 1)).to eq({ shuffle_replicas: true }) + expect(C.validate(shuffle_replicas: 0)).to eq({ shuffle_replicas: true }) end it 'should validate :page_size' do @@ -317,17 +354,41 @@ def unavailable expect(C.validate(page_size: nil)).to eq({ page_size: nil }) end + it 'should validate :protocol_version' do + expect { C.validate(protocol_version: 'a') }.to raise_error(ArgumentError) + expect { C.validate(protocol_version: 0) }.to raise_error(ArgumentError) + expect { C.validate(protocol_version: 1.5) }.to raise_error(ArgumentError) + expect { C.validate(protocol_version: Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION + 1) }. + to raise_error(ArgumentError) + expect(C.validate(protocol_version: 1)).to eq({ protocol_version: 1 }) + expect(C.validate(protocol_version: Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION)). + to eq({ protocol_version: Cassandra::Protocol::Versions::MAX_SUPPORTED_VERSION }) + expect(C.validate(protocol_version: nil)).to eq({ protocol_version: nil }) + end + + it 'should validate that :protocol_version and :allow_beta_protocol are not both specified' do + expect(C.validate(allow_beta_protocol: true)).to eq({ allow_beta_protocol: true } ) + expect(C.validate(allow_beta_protocol: false)).to eq({ allow_beta_protocol: false } ) + expect { C.validate(allow_beta_protocol: true, protocol_version: 3) }.to raise_error(ArgumentError) + expect(C.validate(allow_beta_protocol: false, protocol_version: 3)). + to eq({ allow_beta_protocol: false, protocol_version: 3 }) + expect(C.validate(allow_beta_protocol: true, protocol_version: nil)). + to eq({ allow_beta_protocol: true, protocol_version: nil }) + expect(C.validate(allow_beta_protocol: false, protocol_version: nil)). + to eq({ allow_beta_protocol: false, protocol_version: nil }) + end + it 'should validate :futures_factory option' do class GoodFactory def error end - + def value end - + def promise end - + def all end end @@ -358,15 +419,29 @@ def resolve end it 'should massage :synchronize_schema to a boolean' do - expect(C.validate(synchronize_schema: nil)).to eq({synchronize_schema: false}) - expect(C.validate(synchronize_schema: 1)).to eq({synchronize_schema: true}) - expect(C.validate(synchronize_schema: 0)).to eq({synchronize_schema: true}) + expect(C.validate(synchronize_schema: nil)).to eq({ synchronize_schema: false }) + expect(C.validate(synchronize_schema: 1)).to eq({ synchronize_schema: true }) + expect(C.validate(synchronize_schema: 0)).to eq({ synchronize_schema: true }) end - it 'should massage :client_timestamps to a boolean' do - expect(C.validate(client_timestamps: nil)).to eq({client_timestamps: false}) - expect(C.validate(client_timestamps: 1)).to eq({client_timestamps: true}) - expect(C.validate(client_timestamps: 0)).to eq({client_timestamps: true}) + it 'should map :client_timestamps to a generator class or nil' do + expect(C.validate(client_timestamps: nil)).to eq({ timestamp_generator: nil }) + expect(C.validate(client_timestamps: false)).to eq({ timestamp_generator: nil }) + expect(C.validate({})).to eq({}) + + expected_class = RUBY_ENGINE == 'jruby' ? + Cassandra::TimestampGenerator::TickingOnDuplicate : + Cassandra::TimestampGenerator::Simple + expect(C.validate(client_timestamps: true)[:timestamp_generator]).to be_instance_of(expected_class) + expect { C.validate(client_timestamps: Object.new) }.to raise_error(ArgumentError) + + valid_generator = Object.new + + def valid_generator.next + 42 + end + + expect(C.validate(client_timestamps: valid_generator)).to eq({ timestamp_generator: valid_generator }) end it 'should validate :connections_per_local_node' do diff --git a/spec/regressions/RUBY-189_spec.rb b/spec/regressions/RUBY-189_spec.rb new file mode 100644 index 000000000..91babeb4b --- /dev/null +++ b/spec/regressions/RUBY-189_spec.rb @@ -0,0 +1,111 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'spec_helper' + +module Cassandra + class Cluster + describe(Client) do + let(:hosts) { ['127.0.0.1', '127.0.0.2'] } + let(:io_reactor) { FakeIoReactor.new } + let(:reconnection_policy) { Reconnection::Policies::Exponential.new(0.5, 30, 2) } + let(:load_balancing_policy) { FakeLoadBalancingPolicy.new(cluster_registry) } + let(:cluster_registry) { FakeClusterRegistry.new(hosts) } + let(:logger) { Cassandra::NullLogger.new } + let(:driver_settings) { { + :io_reactor => io_reactor, + :load_balancing_policy => load_balancing_policy, + :cluster_registry => cluster_registry, + :connections_per_local_node => 2, + :connections_per_remote_node => 1, + :reconnection_policy => reconnection_policy, + :executor => Executors::SameThread.new, + :logger => logger, + :protocol_version => 2 + } } + + let(:driver) { Driver.new(driver_settings) } + let(:client) { + Client.new(driver.logger, driver.cluster_registry, driver.cluster_schema, driver.io_reactor, driver.connector, + driver.profile_manager, driver.reconnection_policy, + driver.address_resolution_policy, driver.connection_options, driver.futures_factory, driver.timestamp_generator) + } + + let(:promise) { double('promise') } + let(:plan) { double('plan') } + let(:connection) { double('connection') } + let(:options) { {} } + let(:errors) { {} } + let(:hosts) { [] } + let(:statement) { double('statement') } + let(:request) { double('request') } + let(:batch_statement) { double('batch_statement') } + let(:bound_statement) { double('bound_statement') } + let(:response) { double('response future')} + let(:prepared_id) { 1234 } + + it 'RUBY-189 - handles node down after prepare' do + expect(promise).to_not receive(:break) + expect(statement).to receive(:cql).and_return('select * from foo') + + # We expect the down node to receive a request attempt. + expect(connection).to receive(:send_request).and_return(response) + expect(response).to receive(:map).and_return(response) + expect(response).to receive(:on_complete) + client.send(:prepare_and_send_request_by_plan, + 'down_host', + connection, + promise, + 'keyspace', + statement, + options, + 'request', + plan, + 12, + errors, + hosts, + 0) + end + + it 'RUBY-189 - handles node down after prepare in batch' do + expect(request).to receive(:clear) + expect(connection).to receive(:send_request).and_return(response) + expect(response).to receive(:map).and_return(response) + expect(response).to receive(:on_complete) + + expect(bound_statement).to receive(:is_a?).and_return(true) + expect(bound_statement).to receive(:cql).and_return('select * from foo') + expect(batch_statement).to receive(:statements).and_return([bound_statement]) + expect(promise).to_not receive(:break) + client.send(:batch_and_send_request_by_plan, + 'down_host', + connection, + promise, + 'keyspace', + batch_statement, + request, + options, + plan, + 12, + errors, + hosts, + 0) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 641cbde3a..801fe2a2c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,6 +33,9 @@ require 'support/stub_io_reactor' RSpec.configure do |config| + # suppress ruby warnings + config.warnings = false + config.expect_with(:rspec) do |c| c.syntax = [:should, :expect] end diff --git a/spec/support/await_helper.rb b/spec/support/await_helper.rb index 903475eb4..d1a85ab8e 100644 --- a/spec/support/await_helper.rb +++ b/spec/support/await_helper.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/support/bytes_helper.rb b/spec/support/bytes_helper.rb index 81ca1faf2..f465b9a97 100644 --- a/spec/support/bytes_helper.rb +++ b/spec/support/bytes_helper.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/support/fake_cluster_registry.rb b/spec/support/fake_cluster_registry.rb index 0b73217b7..a6c72c467 100644 --- a/spec/support/fake_cluster_registry.rb +++ b/spec/support/fake_cluster_registry.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -71,6 +71,24 @@ def initialize(fake_cluster_registry) @index = -1 end + def host_up(*args) + end + + def host_down(*args) + end + + def host_found(*args) + end + + def host_lost(*args) + end + + def setup(*args) + end + + def teardown(*args) + end + def distance(host) @registry.hosts.include?(host) ? :local : :ignore end diff --git a/spec/support/fake_io_reactor.rb b/spec/support/fake_io_reactor.rb index 7cf5f0072..33068c753 100644 --- a/spec/support/fake_io_reactor.rb +++ b/spec/support/fake_io_reactor.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/spec/support/stub_io_reactor.rb b/spec/support/stub_io_reactor.rb index 38cf13a81..9cc8214b5 100644 --- a/spec/support/stub_io_reactor.rb +++ b/spec/support/stub_io_reactor.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ #++ class StubIoReactor + include MonitorMixin + class NullObject def method_missing(method, *args, &block) self @@ -385,6 +387,8 @@ def initialize @connections = ::Array.new @timers = ::Array.new @max_conns = ::Hash.new + + mon_initialize end def enable_nodes(ips) @@ -465,21 +469,26 @@ def connect(host, port, options) end def schedule_timer(seconds) - promise = Ione::Promise.new - @timers << Timer.new(promise, Time.now + seconds) - promise.future + synchronize do + promise = Ione::Promise.new + @timers << Timer.new(promise, Time.now + seconds) + promise.future + end end def advance_time(seconds) - @timers.dup.each {|timer| timer.advance(seconds)} - @timers.reject! {|timer| timer.expired?} - + synchronize do + @timers.dup.each { |timer| timer.advance(seconds) } + @timers.reject! { |timer| timer.expired? } + end self end def cancel_timer(timer_future) - @timers.reject! do |timer| - timer.resolves?(timer_future) + synchronize do + @timers.reject! do |timer| + timer.resolves?(timer_future) + end end end end diff --git a/support/ccm.rb b/support/ccm.rb index 30ff6d880..c7ad009c0 100644 --- a/support/ccm.rb +++ b/support/ccm.rb @@ -1,7 +1,7 @@ # encoding: utf-8 #-- -# Copyright 2013-2016 DataStax, Inc. +# Copyright 2013-2017 DataStax, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -455,6 +455,9 @@ def start(jvm_arg=nil) options[:load_balancing_policy] = SameOrderLoadBalancingPolicy.new + enable_triggers + + total_attempts = 1 until @nodes.all?(&:up?) && @cluster && @cluster.hosts.select(&:up?).count == @nodes.size attempts = 1 @@ -493,6 +496,11 @@ def start(jvm_arg=nil) $stderr.puts "not all hosts are up yet, retrying in 1s..." sleep(1) end + + total_attempts += 1 + if total_attempts >= 20 + raise "Cluster hosts did not match node count. nodes:#{@nodes.size}, hosts:#{@cluster.hosts.select(&:up?).count}" + end end $stderr.puts "creating session" @@ -708,6 +716,17 @@ def change_tombstone_thresholds start end + def enable_triggers + trigger_root = File.expand_path(File.dirname(__FILE__) + '/../support/triggers') + ccm_node_conf_dir = "~/.ccm/ruby-driver-cassandra-#{CCM.cassandra_version}-test-cluster" + + (1..@nodes.size).each do |n| + `mkdir -p #{ccm_node_conf_dir}/node#{n}/conf/triggers` + `cp #{trigger_root}/AuditTrigger.properties #{ccm_node_conf_dir}/node#{n}/conf` + `cp #{trigger_root}/trigger-example.jar #{ccm_node_conf_dir}/node#{n}/conf/triggers` + end + end + def setup_schema(schema) schema.strip! schema.chomp!(";") @@ -871,6 +890,15 @@ def setup_cluster(no_dc = 1, no_nodes_per_dc = 3) @current_cluster end + def remove_cluster(name) + cluster = clusters.find {|c| c.name == name} + return unless cluster + ccm.exec('remove', cluster.name) + clusters.delete(cluster) + @current_cluster = nil if @current_cluster.name == name + nil + end + private def ccm @@ -918,22 +946,16 @@ def switch_cluster(name) nil end - def remove_cluster(name) - cluster = clusters.find {|c| c.name == name} - return unless cluster - ccm.exec('remove', cluster.name) - clusters.delete(cluster) - - nil - end - def create_cluster(name, version, datacenters, nodes_per_datacenter) nodes = Array.new(datacenters, nodes_per_datacenter).join(':') - create_args = ['-v', version, name] - create_args << '--dse' if @dse - - ccm.exec('create', *create_args) + if !@dse && ENV['CASSANDRA_DIR'] && !ENV['CASSANDRA_DIR'].empty? + ccm.exec('create', name, '--install-dir', ENV['CASSANDRA_DIR']) + else + create_args = ['-v', version, name] + create_args << '--dse' if @dse + ccm.exec('create', *create_args) + end config = [ '--rt', '1000', diff --git a/support/triggers/AuditTrigger.properties b/support/triggers/AuditTrigger.properties new file mode 100644 index 000000000..a0e5f2011 --- /dev/null +++ b/support/triggers/AuditTrigger.properties @@ -0,0 +1,20 @@ +# encoding: utf-8 + +#-- +# Copyright 2013-2017 DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +keyspace=simplex +table=audit diff --git a/support/triggers/trigger-example.jar b/support/triggers/trigger-example.jar new file mode 100644 index 000000000..fca6515d7 Binary files /dev/null and b/support/triggers/trigger-example.jar differ