From c3fb2faeb3d3d197159cb91a3ea3464e52ae0204 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 16 Mar 2017 16:17:17 -0400 Subject: [PATCH] Rename from generic github-data to specific github-kv --- CONTRIBUTING.md | 4 +- Gemfile | 2 +- README.md | 37 +- examples/example_setup.rb | 2 +- examples/kv.rb | 16 +- github-data.gemspec => github-kv.gemspec | 16 +- .../{data => kv}/active_record_generator.rb | 2 +- .../{data => kv}/templates/migration.rb | 0 lib/github/data.rb | 9 - lib/github/data/kv.rb | 345 ------------------ lib/github/kv.rb | 344 +++++++++++++++++ lib/github/{data => kv}/result.rb | 70 ++-- lib/github/{data => kv}/sql.rb | 36 +- lib/github/{data => kv}/version.rb | 2 +- script/console | 2 +- .../active_record_generator_test.rb | 6 +- test/github/data/result_test.rb | 86 ----- test/github/data_test.rb | 7 - test/github/kv/result_test.rb | 86 +++++ test/github/{data => kv}/sql_test.rb | 66 ++-- test/github/{data => }/kv_test.rb | 24 +- test/test_helper.rb | 2 +- 22 files changed, 573 insertions(+), 591 deletions(-) rename github-data.gemspec => github-kv.gemspec (74%) rename lib/generators/github/{data => kv}/active_record_generator.rb (97%) rename lib/generators/github/{data => kv}/templates/migration.rb (100%) delete mode 100644 lib/github/data.rb delete mode 100644 lib/github/data/kv.rb create mode 100644 lib/github/kv.rb rename lib/github/{data => kv}/result.rb (65%) rename lib/github/{data => kv}/sql.rb (90%) rename lib/github/{data => kv}/version.rb (76%) rename test/generators/github/{data => kv}/active_record_generator_test.rb (81%) delete mode 100644 test/github/data/result_test.rb delete mode 100644 test/github/data_test.rb create mode 100644 test/github/kv/result_test.rb rename test/github/{data => kv}/sql_test.rb (62%) rename test/github/{data => }/kv_test.rb (84%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8751547..887ce44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ ## Contributing -[fork]: https://github.com/github/github-data/fork -[pr]: https://github.com/github/github-data/compare +[fork]: https://github.com/github/github-kv/fork +[pr]: https://github.com/github/github-kv/compare [style]: https://github.com/styleguide/ruby [code-of-conduct]: CODE_OF_CONDUCT.md diff --git a/Gemfile b/Gemfile index 0004dd5..abd0c33 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -# Specify your gem's dependencies in github-data.gemspec +# Specify your gem's dependencies in github-kv.gemspec gemspec gem "rails", "~> #{ENV['RAILS_VERSION'] || '5.0.2'}" diff --git a/README.md b/README.md index d1f1911..a988206 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -# Github::Data +# Github::KV -GitHub::Data is a few handy classes on top of ActiveRecord for working with SQL. +`GitHub::KV` is a key/value data store backed by MySQL, built on top of `GitHub::KV::SQL` and `GitHub::KV::Result`, each of which are useful on their own apart from KV. -* `GitHub::Data::KV` is a key/value data store backed by MySQL, built on top of `SQL` and `Result`. -* `GitHub::Data::SQL` is for building and executing a SQL query. This class uses ActiveRecord's connection class, but provides a better API for bind values and raw data access. -* `GitHub::Data::Result` makes it easier to bake in resiliency through the use of a Result object instead of raising exceptions. +* `GitHub::KV::SQL` is for building and executing a SQL query. This class uses ActiveRecord's connection class, but provides a better API for bind values and raw data access. +* `GitHub::KV::Result` makes it easier to bake in resiliency through the use of a Result object instead of raising exceptions. ## Installation Add this line to your application's Gemfile: ```ruby -gem 'github-data' +gem 'github-kv' ``` And then execute: @@ -20,16 +19,16 @@ And then execute: Or install it yourself as: - $ gem install github-data + $ gem install github-kv ## Usage -### GitHub::Data::KV +### GitHub::KV -First, you'll need to create the key_values table using the included Rails migration generator. +First, you'll need to create the `key_values` table using the included Rails migration generator. ``` -rails generate github:data:active_record +rails generate github:kv:active_record rails db:migrate ``` @@ -37,14 +36,14 @@ Once you have the table, KV can do neat things like this: ```ruby require "pp" -require "github/data/kv" +require "github/kv" # Create new instance using ActiveRecord's default connection. -kv = GitHub::Data::KV.new { ActiveRecord::Base.connection } +kv = GitHub::KV.new { ActiveRecord::Base.connection } # Get a key. pp kv.get("foo") -# +# # Set a key. kv.set("foo", "bar") @@ -52,23 +51,23 @@ kv.set("foo", "bar") # Get the key again. pp kv.get("foo") -# +# # Get multiple keys at once. pp kv.mget(["foo", "bar"]) -# +# # Check for existence of a key. pp kv.exists("foo") -# +# # Check for existence of key that does not exist. pp kv.exists("bar") -# +# # Check for existence of multiple keys at once. pp kv.mexists(["foo", "bar"]) -# +# # Set a key's value if the key does not already exist. pp kv.setnx("foo", "bar") @@ -91,7 +90,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/github/github-data. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. +Bug reports and pull requests are welcome on GitHub at https://github.com/github/github-kv. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License diff --git a/examples/example_setup.rb b/examples/example_setup.rb index 6cad91d..f024e4d 100644 --- a/examples/example_setup.rb +++ b/examples/example_setup.rb @@ -9,7 +9,7 @@ require "active_record" ActiveRecord::Base.establish_connection({ adapter: "mysql2", - database: "github_data_test", + database: "github_kv_test", }) ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS `key_values`") diff --git a/examples/kv.rb b/examples/kv.rb index 27e425c..8d00248 100644 --- a/examples/kv.rb +++ b/examples/kv.rb @@ -1,12 +1,12 @@ require File.expand_path("../example_setup", __FILE__) -require "github/data/kv" +require "github/kv" # Create new instance using ActiveRecord's default connection. -kv = GitHub::Data::KV.new { ActiveRecord::Base.connection } +kv = GitHub::KV.new { ActiveRecord::Base.connection } # Get a key. pp kv.get("foo") -# +# # Set a key. kv.set("foo", "bar") @@ -14,23 +14,23 @@ kv.set("foo", "bar") # Get the key again. pp kv.get("foo") -# +# # Get multiple keys at once. pp kv.mget(["foo", "bar"]) -# +# # Check for existence of a key. pp kv.exists("foo") -# +# # Check for existence of key that does not exist. pp kv.exists("bar") -# +# # Check for existence of multiple keys at once. pp kv.mexists(["foo", "bar"]) -# +# # Set a key's value if the key does not already exist. pp kv.setnx("foo", "bar") diff --git a/github-data.gemspec b/github-kv.gemspec similarity index 74% rename from github-data.gemspec rename to github-kv.gemspec index cd39eac..d86f041 100644 --- a/github-data.gemspec +++ b/github-kv.gemspec @@ -1,23 +1,23 @@ # coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'github/data/version' +require "github/kv/version" Gem::Specification.new do |spec| - spec.name = "github-data" - spec.version = Github::Data::VERSION + spec.name = "github-kv" + spec.version = Github::KV::VERSION spec.authors = ["GitHub Open Source", "John Nunemaker"] - spec.email = ["opensource+github-data@github.com", "nunemaker@gmail.com"] + spec.email = ["opensource+github-kv@github.com", "nunemaker@gmail.com"] - spec.summary = %q{Useful tools for working with SQL data.} - spec.description = %q{Useful tools for working with SQL data.} - spec.homepage = "https://github.com/github/github-data" + spec.summary = %q{A key/value data store backed by MySQL.} + spec.description = %q{A key/value data store backed by MySQL.} + spec.homepage = "https://github.com/github/github-kv" spec.license = "MIT" # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' # to allow pushing to a single host or delete this section to allow pushing to any host. if spec.respond_to?(:metadata) - spec.metadata['allowed_push_host'] = "https://rubygems.org" + spec.metadata["allowed_push_host"] = "https://rubygems.org" else raise "RubyGems 2.0 or newer is required to protect against " \ "public gem pushes." diff --git a/lib/generators/github/data/active_record_generator.rb b/lib/generators/github/kv/active_record_generator.rb similarity index 97% rename from lib/generators/github/data/active_record_generator.rb rename to lib/generators/github/kv/active_record_generator.rb index be35c9f..be1a672 100644 --- a/lib/generators/github/data/active_record_generator.rb +++ b/lib/generators/github/kv/active_record_generator.rb @@ -1,7 +1,7 @@ require 'rails/generators/active_record' module Github - module Data + class KV module Generators class ActiveRecordGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration diff --git a/lib/generators/github/data/templates/migration.rb b/lib/generators/github/kv/templates/migration.rb similarity index 100% rename from lib/generators/github/data/templates/migration.rb rename to lib/generators/github/kv/templates/migration.rb diff --git a/lib/github/data.rb b/lib/github/data.rb deleted file mode 100644 index f4bd33b..0000000 --- a/lib/github/data.rb +++ /dev/null @@ -1,9 +0,0 @@ -require "github/data/version" -require "github/data/result" -require "github/data/sql" -require "github/data/kv" - -module Github - module Data - end -end diff --git a/lib/github/data/kv.rb b/lib/github/data/kv.rb deleted file mode 100644 index ce08d10..0000000 --- a/lib/github/data/kv.rb +++ /dev/null @@ -1,345 +0,0 @@ -require "github/data/result" -require "github/data/sql" - -# GitHub::Data::KV is a key/value data store backed by MySQL (however, the backing -# store used should be regarded as an implementation detail). -# -# Usage tips: -# -# * Components in key names should be ordered by cardinality, from lowest to -# highest. That is, static key components should be at the front of the key -# and key components that vary should be at the end of the key in order of -# how many potential values they might have. -# -# For example, if using GitHub::Data::KV to store a user preferences, the key -# should be named "user.#{preference_name}.#{user_id}". Notice that the -# part of the key that never changes ("user") comes first, followed by -# the name of the preference (of which there might be a handful), followed -# finally by the user id (of which there are millions). -# -# This will make it easier to scan for keys later on, which is a necessity -# if we ever need to move this data out of GitHub::Data::KV or if we need to -# search the keyspace for some reason (for example, if it's a preference -# that we're planning to deprecate, putting the preference name near the -# beginning of the key name makes it easier to search for all users with -# that preference set). -# -# * All reader methods in GitHub::Data::KV return values wrapped inside a Result -# object. -# -# If any of these methods raise an exception for some reason (for example, -# the database is down), they will return a Result value representing this -# error rather than raising the exception directly. See lib/github/data/result.rb -# for more documentation on GitHub::Data::Result including usage examples. -# -# When using GitHub::Data::KV, it's important to handle error conditions and not -# assume that GitHub::Data::Result objects will always represent success. -# Code using GitHub::Data::KV should be able to fail partially if -# GitHub::Data::KV is down. How exactly to do this will depend on a -# case-by-case basis - it may involve falling back to a default value, or it -# might involve showing an error message to the user while still letting the -# rest of the page load. -# -module GitHub - module Data - class KV - MAX_KEY_LENGTH = 255 - MAX_VALUE_LENGTH = 65535 - - KeyLengthError = Class.new(StandardError) - ValueLengthError = Class.new(StandardError) - UnavailableError = Class.new(StandardError) - - class MissingConnectionError < StandardError; end - - def initialize(encapsulated_errors = [SystemCallError], &conn_block) - @encapsulated_errors = encapsulated_errors - @conn_block = conn_block - end - - def connection - @conn_block.try(:call) || (raise MissingConnectionError, "KV must be initialized with a block that returns a connection") - end - - # get :: String -> Result - # - # Gets the value of the specified key. - # - # Example: - # - # kv.get("foo") - # # => # - # - # kv.get("octocat") - # # => # - # - def get(key) - validate_key(key) - - mget([key]).map { |values| values[0] } - end - - # mget :: [String] -> Result<[String | nil]> - # - # Gets the values of all specified keys. Values will be returned in the - # same order as keys are specified. nil will be returned in place of a - # String for keys which do not exist. - # - # Example: - # - # kv.mget(["foo", "octocat"]) - # # => # keys, :connection => connection).to_h - SELECT `key`, value FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > NOW()) - SQL - - keys.map { |key| kvs[key] } - } - end - - # set :: String, String, expires: Time? -> nil - # - # Sets the specified key to the specified value. Returns nil. Raises on - # error. - # - # Example: - # - # kv.set("foo", "bar") - # # => nil - # - def set(key, value, expires: nil) - validate_key(key) - validate_value(value) - - mset({ key => value }, expires: expires) - end - - # mset :: { String => String }, expires: Time? -> nil - # - # Sets the specified hash keys to their associated values, setting them to - # expire at the specified time. Returns nil. Raises on error. - # - # Example: - # - # kv.mset({ "foo" => "bar", "baz" => "quux" }) - # # => nil - # - # kv.mset({ "expires" => "soon" }, expires: 1.hour.from_now) - # # => nil - # - def mset(kvs, expires: nil) - validate_key_value_hash(kvs) - validate_expires(expires) if expires - - rows = kvs.map { |key, value| - [key, value, GitHub::Data::SQL::NOW, GitHub::Data::SQL::NOW, expires || GitHub::Data::SQL::NULL] - } - - encapsulate_error do - GitHub::Data::SQL.run(<<-SQL, :rows => GitHub::Data::SQL::ROWS(rows), :connection => connection) - INSERT INTO key_values (`key`, value, created_at, updated_at, expires_at) - VALUES :rows - ON DUPLICATE KEY UPDATE - value = VALUES(value), - updated_at = VALUES(updated_at), - expires_at = VALUES(expires_at) - SQL - end - - nil - end - - # exists :: String -> Result - # - # Checks for existence of the specified key. - # - # Example: - # - # kv.exists("foo") - # # => # - # - # kv.exists("octocat") - # # => # - # - def exists(key) - validate_key(key) - - mexists([key]).map { |values| values[0] } - end - - # mexists :: [String] -> Result<[Boolean]> - # - # Checks for existence of all specified keys. Booleans will be returned in - # the same order as keys are specified. - # - # Example: - # - # kv.mexists(["foo", "octocat"]) - # # => # - # - def mexists(keys) - validate_key_array(keys) - - Result.new { - existing_keys = GitHub::Data::SQL.values(<<-SQL, :keys => keys, :connection => connection).to_set - SELECT `key` FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > NOW()) - SQL - - keys.map { |key| existing_keys.include?(key) } - } - end - - # setnx :: String, String, expires: Time? -> Boolean - # - # Sets the specified key to the specified value only if it does not - # already exist. - # - # Returns true if the key was set, false otherwise. Raises on error. - # - # Example: - # - # kv.setnx("foo", "bar") - # # => false - # - # kv.setnx("octocat", "monalisa") - # # => true - # - # kv.setnx("expires", "soon", expires: 1.hour.from_now) - # # => true - # - def setnx(key, value, expires: nil) - validate_key(key) - validate_value(value) - validate_expires(expires) if expires - - encapsulate_error { - # if the key already exists but has expired, prune it first. We could - # achieve the same thing with the right INSERT ... ON DUPLICATE KEY UPDATE - # query, but then we would not be able to rely on affected_rows - - GitHub::Data::SQL.run(<<-SQL, :key => key, :connection => connection) - DELETE FROM key_values WHERE `key` = :key AND expires_at <= NOW() - SQL - - sql = GitHub::Data::SQL.run(<<-SQL, :key => key, :value => value, :expires => expires || GitHub::Data::SQL::NULL, :connection => connection) - INSERT IGNORE INTO key_values (`key`, value, created_at, updated_at, expires_at) - VALUES (:key, :value, NOW(), NOW(), :expires) - SQL - - sql.affected_rows > 0 - } - end - - # del :: String -> nil - # - # Deletes the specified key. Returns nil. Raises on error. - # - # Example: - # - # kv.del("foo") - # # => nil - # - def del(key) - validate_key(key) - - mdel([key]) - end - - # mdel :: String -> nil - # - # Deletes the specified keys. Returns nil. Raises on error. - # - # Example: - # - # kv.mdel(["foo", "octocat"]) - # # => nil - # - def mdel(keys) - validate_key_array(keys) - - encapsulate_error do - GitHub::Data::SQL.run(<<-SQL, :keys => keys, :connection => connection) - DELETE FROM key_values WHERE `key` IN :keys - SQL - end - - nil - end - - private - def validate_key(key) - raise TypeError, "key must be a String in #{self.class.name}, but was #{key.class}" unless key.is_a?(String) - - validate_key_length(key) - end - - def validate_value(value) - raise TypeError, "value must be a String in #{self.class.name}, but was #{value.class}" unless value.is_a?(String) - - validate_value_length(value) - end - - def validate_key_array(keys) - unless keys.is_a?(Array) - raise TypeError, "keys must be a [String] in #{self.class.name}, but was #{keys.class}" - end - - keys.each do |key| - unless key.is_a?(String) - raise TypeError, "keys must be a [String] in #{self.class.name}, but also saw at least one #{key.class}" - end - - validate_key_length(key) - end - end - - def validate_key_value_hash(kvs) - unless kvs.is_a?(Hash) - raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but was #{key.class}" - end - - kvs.each do |key, value| - unless key.is_a?(String) - raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but also saw at least one key of type #{key.class}" - end - - unless value.is_a?(String) - raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but also saw at least one value of type #{value.class}" - end - - validate_key_length(key) - validate_value_length(value) - end - end - - def validate_key_length(key) - if key.length > MAX_KEY_LENGTH - raise KeyLengthError, "key of length #{key.length} exceeds maximum key length of #{MAX_KEY_LENGTH}\n\nkey: #{key.inspect}" - end - end - - def validate_value_length(value) - if value.length > MAX_VALUE_LENGTH - raise ValueLengthError, "value of length #{value.length} exceeds maximum value length of #{MAX_VALUE_LENGTH}" - end - end - - def validate_expires(expires) - unless expires.respond_to?(:to_time) - raise TypeError, "expires must be a time of some sort (Time, DateTime, ActiveSupport::TimeWithZone, etc.), but was #{expires.class}" - end - end - - def encapsulate_error - yield - rescue *@encapsulated_errors => error - raise UnavailableError, "#{error.class}: #{error.message}" - end - end - end -end diff --git a/lib/github/kv.rb b/lib/github/kv.rb new file mode 100644 index 0000000..3fb34ac --- /dev/null +++ b/lib/github/kv.rb @@ -0,0 +1,344 @@ +require "github/kv/version" +require "github/kv/result" +require "github/kv/sql" + +# GitHub::KV is a key/value data store backed by MySQL (however, the backing +# store used should be regarded as an implementation detail). +# +# Usage tips: +# +# * Components in key names should be ordered by cardinality, from lowest to +# highest. That is, static key components should be at the front of the key +# and key components that vary should be at the end of the key in order of +# how many potential values they might have. +# +# For example, if using GitHub::KV to store a user preferences, the key +# should be named "user.#{preference_name}.#{user_id}". Notice that the +# part of the key that never changes ("user") comes first, followed by +# the name of the preference (of which there might be a handful), followed +# finally by the user id (of which there are millions). +# +# This will make it easier to scan for keys later on, which is a necessity +# if we ever need to move this data out of GitHub::KV or if we need to +# search the keyspace for some reason (for example, if it's a preference +# that we're planning to deprecate, putting the preference name near the +# beginning of the key name makes it easier to search for all users with +# that preference set). +# +# * All reader methods in GitHub::KV return values wrapped inside a Result +# object. +# +# If any of these methods raise an exception for some reason (for example, +# the database is down), they will return a Result value representing this +# error rather than raising the exception directly. See lib/github/kv/result.rb +# for more documentation on GitHub::KV::Result including usage examples. +# +# When using GitHub::KV, it's important to handle error conditions and not +# assume that GitHub::KV::Result objects will always represent success. +# Code using GitHub::KV should be able to fail partially if +# GitHub::KV is down. How exactly to do this will depend on a +# case-by-case basis - it may involve falling back to a default value, or it +# might involve showing an error message to the user while still letting the +# rest of the page load. +# +module GitHub + class KV + MAX_KEY_LENGTH = 255 + MAX_VALUE_LENGTH = 65535 + + KeyLengthError = Class.new(StandardError) + ValueLengthError = Class.new(StandardError) + UnavailableError = Class.new(StandardError) + + class MissingConnectionError < StandardError; end + + def initialize(encapsulated_errors = [SystemCallError], &conn_block) + @encapsulated_errors = encapsulated_errors + @conn_block = conn_block + end + + def connection + @conn_block.try(:call) || (raise MissingConnectionError, "KV must be initialized with a block that returns a connection") + end + + # get :: String -> Result + # + # Gets the value of the specified key. + # + # Example: + # + # kv.get("foo") + # # => # + # + # kv.get("octocat") + # # => # + # + def get(key) + validate_key(key) + + mget([key]).map { |values| values[0] } + end + + # mget :: [String] -> Result<[String | nil]> + # + # Gets the values of all specified keys. Values will be returned in the + # same order as keys are specified. nil will be returned in place of a + # String for keys which do not exist. + # + # Example: + # + # kv.mget(["foo", "octocat"]) + # # => # keys, :connection => connection).to_h + SELECT `key`, value FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > NOW()) + SQL + + keys.map { |key| kvs[key] } + } + end + + # set :: String, String, expires: Time? -> nil + # + # Sets the specified key to the specified value. Returns nil. Raises on + # error. + # + # Example: + # + # kv.set("foo", "bar") + # # => nil + # + def set(key, value, expires: nil) + validate_key(key) + validate_value(value) + + mset({ key => value }, expires: expires) + end + + # mset :: { String => String }, expires: Time? -> nil + # + # Sets the specified hash keys to their associated values, setting them to + # expire at the specified time. Returns nil. Raises on error. + # + # Example: + # + # kv.mset({ "foo" => "bar", "baz" => "quux" }) + # # => nil + # + # kv.mset({ "expires" => "soon" }, expires: 1.hour.from_now) + # # => nil + # + def mset(kvs, expires: nil) + validate_key_value_hash(kvs) + validate_expires(expires) if expires + + rows = kvs.map { |key, value| + [key, value, GitHub::KV::SQL::NOW, GitHub::KV::SQL::NOW, expires || GitHub::KV::SQL::NULL] + } + + encapsulate_error do + GitHub::KV::SQL.run(<<-SQL, :rows => GitHub::KV::SQL::ROWS(rows), :connection => connection) + INSERT INTO key_values (`key`, value, created_at, updated_at, expires_at) + VALUES :rows + ON DUPLICATE KEY UPDATE + value = VALUES(value), + updated_at = VALUES(updated_at), + expires_at = VALUES(expires_at) + SQL + end + + nil + end + + # exists :: String -> Result + # + # Checks for existence of the specified key. + # + # Example: + # + # kv.exists("foo") + # # => # + # + # kv.exists("octocat") + # # => # + # + def exists(key) + validate_key(key) + + mexists([key]).map { |values| values[0] } + end + + # mexists :: [String] -> Result<[Boolean]> + # + # Checks for existence of all specified keys. Booleans will be returned in + # the same order as keys are specified. + # + # Example: + # + # kv.mexists(["foo", "octocat"]) + # # => # + # + def mexists(keys) + validate_key_array(keys) + + Result.new { + existing_keys = GitHub::KV::SQL.values(<<-SQL, :keys => keys, :connection => connection).to_set + SELECT `key` FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > NOW()) + SQL + + keys.map { |key| existing_keys.include?(key) } + } + end + + # setnx :: String, String, expires: Time? -> Boolean + # + # Sets the specified key to the specified value only if it does not + # already exist. + # + # Returns true if the key was set, false otherwise. Raises on error. + # + # Example: + # + # kv.setnx("foo", "bar") + # # => false + # + # kv.setnx("octocat", "monalisa") + # # => true + # + # kv.setnx("expires", "soon", expires: 1.hour.from_now) + # # => true + # + def setnx(key, value, expires: nil) + validate_key(key) + validate_value(value) + validate_expires(expires) if expires + + encapsulate_error { + # if the key already exists but has expired, prune it first. We could + # achieve the same thing with the right INSERT ... ON DUPLICATE KEY UPDATE + # query, but then we would not be able to rely on affected_rows + + GitHub::KV::SQL.run(<<-SQL, :key => key, :connection => connection) + DELETE FROM key_values WHERE `key` = :key AND expires_at <= NOW() + SQL + + sql = GitHub::KV::SQL.run(<<-SQL, :key => key, :value => value, :expires => expires || GitHub::KV::SQL::NULL, :connection => connection) + INSERT IGNORE INTO key_values (`key`, value, created_at, updated_at, expires_at) + VALUES (:key, :value, NOW(), NOW(), :expires) + SQL + + sql.affected_rows > 0 + } + end + + # del :: String -> nil + # + # Deletes the specified key. Returns nil. Raises on error. + # + # Example: + # + # kv.del("foo") + # # => nil + # + def del(key) + validate_key(key) + + mdel([key]) + end + + # mdel :: String -> nil + # + # Deletes the specified keys. Returns nil. Raises on error. + # + # Example: + # + # kv.mdel(["foo", "octocat"]) + # # => nil + # + def mdel(keys) + validate_key_array(keys) + + encapsulate_error do + GitHub::KV::SQL.run(<<-SQL, :keys => keys, :connection => connection) + DELETE FROM key_values WHERE `key` IN :keys + SQL + end + + nil + end + + private + def validate_key(key) + raise TypeError, "key must be a String in #{self.class.name}, but was #{key.class}" unless key.is_a?(String) + + validate_key_length(key) + end + + def validate_value(value) + raise TypeError, "value must be a String in #{self.class.name}, but was #{value.class}" unless value.is_a?(String) + + validate_value_length(value) + end + + def validate_key_array(keys) + unless keys.is_a?(Array) + raise TypeError, "keys must be a [String] in #{self.class.name}, but was #{keys.class}" + end + + keys.each do |key| + unless key.is_a?(String) + raise TypeError, "keys must be a [String] in #{self.class.name}, but also saw at least one #{key.class}" + end + + validate_key_length(key) + end + end + + def validate_key_value_hash(kvs) + unless kvs.is_a?(Hash) + raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but was #{key.class}" + end + + kvs.each do |key, value| + unless key.is_a?(String) + raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but also saw at least one key of type #{key.class}" + end + + unless value.is_a?(String) + raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but also saw at least one value of type #{value.class}" + end + + validate_key_length(key) + validate_value_length(value) + end + end + + def validate_key_length(key) + if key.length > MAX_KEY_LENGTH + raise KeyLengthError, "key of length #{key.length} exceeds maximum key length of #{MAX_KEY_LENGTH}\n\nkey: #{key.inspect}" + end + end + + def validate_value_length(value) + if value.length > MAX_VALUE_LENGTH + raise ValueLengthError, "value of length #{value.length} exceeds maximum value length of #{MAX_VALUE_LENGTH}" + end + end + + def validate_expires(expires) + unless expires.respond_to?(:to_time) + raise TypeError, "expires must be a time of some sort (Time, DateTime, ActiveSupport::TimeWithZone, etc.), but was #{expires.class}" + end + end + + def encapsulate_error + yield + rescue *@encapsulated_errors => error + raise UnavailableError, "#{error.class}: #{error.message}" + end + end +end diff --git a/lib/github/data/result.rb b/lib/github/kv/result.rb similarity index 65% rename from lib/github/data/result.rb rename to lib/github/kv/result.rb index bbe492f..887ebb1 100644 --- a/lib/github/data/result.rb +++ b/lib/github/kv/result.rb @@ -1,18 +1,18 @@ module GitHub - module Data + class KV class Result # Invokes the supplied block and wraps the return value in a - # GitHub::Data::Result object. + # GitHub::KV::Result object. # # Exceptions raised by the block are caught and also wrapped. # # Example: # - # GitHub::Data::Result.new { 123 } - # # => # + # GitHub::KV::Result.new { 123 } + # # => # # - # GitHub::Data::Result.new { raise "oops" } - # # => #> + # GitHub::KV::Result.new { raise "oops" } + # # => #> # def initialize begin @@ -25,9 +25,9 @@ module GitHub def to_s if ok? - "#" % [object_id, @value.inspect] + "#" % [object_id, @value.inspect] else - "#" % [object_id, @error.inspect] + "#" % [object_id, @error.inspect] end end @@ -38,7 +38,7 @@ module GitHub # # If the result represents an error, returns self. # - # The block must also return a GitHub::Data::Result object. + # The block must also return a GitHub::KV::Result object. # Use #map otherwise. # # Example: @@ -46,17 +46,17 @@ module GitHub # result = do_something().then { |val| # do_other_thing(val) # } - # # => # + # # => # # # do_something_that_fails().then { |val| # # never invoked # } - # # => # + # # => # # def then if ok? result = yield(@value) - raise TypeError, "block invoked in GitHub::Data::Result#then did not return GitHub::Data::Result" unless result.is_a?(Result) + raise TypeError, "block invoked in GitHub::KV::Result#then did not return GitHub::KV::Result" unless result.is_a?(Result) result else self @@ -67,7 +67,7 @@ module GitHub # # If the result represents a value, returns self. # - # The block must also return a GitHub::Data::Result object. + # The block must also return a GitHub::KV::Result object. # Use #map otherwise. # # Example: @@ -75,41 +75,41 @@ module GitHub # result = do_something().rescue { |val| # # never invoked # } - # # => # + # # => # # # do_something_that_fails().rescue { |val| # # handle_error(val) # } - # # => # + # # => # # def rescue return self if ok? result = yield(@error) - raise TypeError, "block invoked in GitHub::Data::Result#rescue did not return GitHub::Data::Result" unless result.is_a?(Result) + raise TypeError, "block invoked in GitHub::KV::Result#rescue did not return GitHub::KV::Result" unless result.is_a?(Result) result end # If the result represents a value, invokes the supplied block with that - # value and wraps the block's return value in a GitHub::Data::Result. + # value and wraps the block's return value in a GitHub::KV::Result. # # If the result represents an error, returns self. # - # The block should not return a GitHub::Data::Result object (unless you - # truly intend to create a GitHub::Data::Result>). + # The block should not return a GitHub::KV::Result object (unless you + # truly intend to create a GitHub::KV::Result>). # Use #then if it does. # # Example: # # result = do_something() - # # => # + # # => # # # result.map { |val| val * 2 } - # # => # + # # => # # # do_something_that_fails().map { |val| # # never invoked # } - # # => # + # # => # # def map if ok? @@ -127,20 +127,20 @@ module GitHub # Example: # # result = do_something() - # # => # + # # => # # # result.value { "nope" } # # => "foo" # # result = do_something_that_fails() - # # => # + # # => # # # result.value { "nope" } - # # => # + # # => # # def value unless block_given? - raise ArgumentError, "must provide a block to GitHub::Data::Result#value to be invoked in case of error" + raise ArgumentError, "must provide a block to GitHub::KV::Result#value to be invoked in case of error" end if ok? @@ -157,13 +157,13 @@ module GitHub # Example: # # result = do_something() - # # => # + # # => # # # result.value! # # => "foo" # # result = do_something_that_fails() - # # => # + # # => # # # result.value! # # !! raises exception @@ -181,13 +181,13 @@ module GitHub # Example: # # result = do_something() - # # => # + # # => # # # result.ok? # # => true # # result = do_something_that_fails() - # # => # + # # => # # # result.ok? # # => false @@ -201,13 +201,13 @@ module GitHub # If the result represents an error, returns that error. # # result = do_something() - # # => # + # # => # # # result.error # # => nil # # result = do_something_that_fails() - # # => # + # # => # # # result.error # # => ... @@ -216,10 +216,10 @@ module GitHub @error end - # Create a GitHub::Data::Result with only the error condition set. + # Create a GitHub::KV::Result with only the error condition set. # - # GitHub::Data::Result.error(e) - # # => # + # GitHub::KV::Result.error(e) + # # => # # def self.error(e) result = allocate diff --git a/lib/github/data/sql.rb b/lib/github/kv/sql.rb similarity index 90% rename from lib/github/data/sql.rb rename to lib/github/kv/sql.rb index d9639fe..6ecd213 100644 --- a/lib/github/data/sql.rb +++ b/lib/github/kv/sql.rb @@ -2,14 +2,14 @@ require "active_record" require "active_support/all" module GitHub - module Data + class KV # Public: Build and execute a SQL query, returning results as Arrays. This # class uses ActiveRecord's connection classes, but provides a better API for # bind values and raw data access. # # Example: # - # sql = GitHub::Data::SQL.new(<<-SQL, :parent_ids => parent_ids, :network_id => network_id) + # sql = GitHub::KV::SQL.new(<<-SQL, :parent_ids => parent_ids, :network_id => network_id) # SELECT * FROM repositories # WHERE source_id = :network_id AND parent_id IN :parent_ids # SQL @@ -30,7 +30,7 @@ module GitHub # # * Arrays are escaped as `(item, item, item)`. If you need to insert multiple # rows (Arrays of Arrays), you must specify the bind value using - # GitHub::Data::SQL::ROWS(array_of_arrays). + # GitHub::KV::SQL::ROWS(array_of_arrays). # class SQL @@ -78,7 +78,7 @@ module GitHub # Used when a column contains binary data which needs to be escaped # to prevent warnings from MySQL def self.BINARY(string) - GitHub::Data::SQL.LITERAL(GitHub::Data::SQL.BINARY_LITERAL(string)) + GitHub::KV::SQL.LITERAL(GitHub::KV::SQL.BINARY_LITERAL(string)) end # Public: Escape a binary SQL value, yielding a string which can be used as @@ -158,7 +158,7 @@ module GitHub # aren't available to subsequent adds. # # Returns self. - # Raises GitHub::Data::SQL::BadBind for unknown keyword tokens. + # Raises GitHub::KV::SQL::BadBind for unknown keyword tokens. def add(sql, extras = nil) return self if sql.blank? @@ -186,7 +186,7 @@ module GitHub # aren't available to subsequent adds. # # Returns self. - # Raises GitHub::Data::SQL::BadBind for unknown keyword tokens. + # Raises GitHub::KV::SQL::BadBind for unknown keyword tokens. def add_unless_empty(sql, extras = nil) return self if query.empty? add sql, extras @@ -315,8 +315,8 @@ module GitHub # Public: Execute, ignoring results. This is useful when the results of a # query aren't important, often INSERTs, UPDATEs, or DELETEs. # - # sql - An optional SQL string. See GitHub::Data::SQL#add for details. - # extras - Optional bind values. See GitHub::Data::SQL#add for details. + # sql - An optional SQL string. See GitHub::KV::SQL#add for details. + # extras - Optional bind values. See GitHub::KV::SQL#add for details. # # Returns self. def run(sql = nil, extras = nil) @@ -337,8 +337,8 @@ module GitHub # Public: Create and execute a new SQL query, ignoring results. # - # sql - A SQL string. See GitHub::Data::SQL#add for details. - # bindings - Optional bind values. See GitHub::Data::SQL#add for details. + # sql - A SQL string. See GitHub::KV::SQL#add for details. + # bindings - Optional bind values. See GitHub::KV::SQL#add for details. # # Returns self. def self.run(sql, bindings = {}) @@ -347,8 +347,8 @@ module GitHub # Public: Create and execute a new SQL query, returning its hash_result rows. # - # sql - A SQL string. See GitHub::Data::SQL#add for details. - # bindings - Optional bind values. See GitHub::Data::SQL#add for details. + # sql - A SQL string. See GitHub::KV::SQL#add for details. + # bindings - Optional bind values. See GitHub::KV::SQL#add for details. # # Returns an Array of result hashes. def self.hash_results(sql, bindings = {}) @@ -357,8 +357,8 @@ module GitHub # Public: Create and execute a new SQL query, returning its result rows. # - # sql - A SQL string. See GitHub::Data::SQL#add for details. - # bindings - Optional bind values. See GitHub::Data::SQL#add for details. + # sql - A SQL string. See GitHub::KV::SQL#add for details. + # bindings - Optional bind values. See GitHub::KV::SQL#add for details. # # Returns an Array of result arrays. def self.results(sql, bindings = {}) @@ -368,8 +368,8 @@ module GitHub # Public: Create and execute a new SQL query, returning the value of the # first column of the first result row. # - # sql - A SQL string. See GitHub::Data::SQL#add for details. - # bindings - Optional bind values. See GitHub::Data::SQL#add for details. + # sql - A SQL string. See GitHub::KV::SQL#add for details. + # bindings - Optional bind values. See GitHub::KV::SQL#add for details. # # Returns a value or nil. def self.value(sql, bindings = {}) @@ -378,8 +378,8 @@ module GitHub # Public: Create and execute a new SQL query, returning its values. # - # sql - A SQL string. See GitHub::Data::SQL#add for details. - # bindings - Optional bind values. See GitHub::Data::SQL#add for details. + # sql - A SQL string. See GitHub::KV::SQL#add for details. + # bindings - Optional bind values. See GitHub::KV::SQL#add for details. # # Returns an Array of values. def self.values(sql, bindings = {}) diff --git a/lib/github/data/version.rb b/lib/github/kv/version.rb similarity index 76% rename from lib/github/data/version.rb rename to lib/github/kv/version.rb index d769246..ddca2d8 100644 --- a/lib/github/data/version.rb +++ b/lib/github/kv/version.rb @@ -1,5 +1,5 @@ module Github - module Data + class KV VERSION = "0.1.0" end end diff --git a/script/console b/script/console index 71c74c5..97cab78 100755 --- a/script/console +++ b/script/console @@ -1,7 +1,7 @@ #!/usr/bin/env ruby require "bundler/setup" -require "github/data" +require "github/kv" # 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. diff --git a/test/generators/github/data/active_record_generator_test.rb b/test/generators/github/kv/active_record_generator_test.rb similarity index 81% rename from test/generators/github/data/active_record_generator_test.rb rename to test/generators/github/kv/active_record_generator_test.rb index 06d8eac..738d33d 100644 --- a/test/generators/github/data/active_record_generator_test.rb +++ b/test/generators/github/kv/active_record_generator_test.rb @@ -3,10 +3,10 @@ require "rails" require "rails/test_help" require "active_record" require "rails/generators/test_case" -require "generators/github/data/active_record_generator" +require "generators/github/kv/active_record_generator" -class GitHubDataActiveRecordGeneratorTest < Rails::Generators::TestCase - tests Github::Data::Generators::ActiveRecordGenerator +class GithubKVActiveRecordGeneratorTest < Rails::Generators::TestCase + tests Github::KV::Generators::ActiveRecordGenerator destination File.expand_path("../../../../tmp", __FILE__) setup :prepare_destination diff --git a/test/github/data/result_test.rb b/test/github/data/result_test.rb deleted file mode 100644 index d993905..0000000 --- a/test/github/data/result_test.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'test_helper' - -class GitHub::Data::ResultTest < Minitest::Test - def test_to_s - assert_match %r{#}, GitHub::Data::Result.new { 123 }.to_s - - assert_match %r{#>}, GitHub::Data::Result.new { raise "nope" }.to_s - end - - def test_then - assert_equal 456, GitHub::Data::Result.new { 123 }.then { - GitHub::Data::Result.new { 456 } - }.value! - - assert GitHub::Data::Result.new { raise "nope" }.then { - flunk "should not have invoked then block" - }.error - - assert_raises TypeError do - GitHub::Data::Result.new {}.then { - "not a result" - } - end - end - - def test_rescue - assert_equal 456, GitHub::Data::Result.new { raise "nope" }.rescue { - GitHub::Data::Result.new { 456 } - }.value! - - assert_equal 456, GitHub::Data::Result.new { raise "nope" }.rescue { |error| - assert_equal "nope", error.message - GitHub::Data::Result.new { 456 } - }.value! - - assert GitHub::Data::Result.new { 123 }.rescue { - flunk "should not have invoked rescue block" - }.value! - - assert_raises TypeError do - GitHub::Data::Result.new { raise "nope" }.rescue { - "not a result" - } - end - end - - def test_map - assert_equal 456, GitHub::Data::Result.new { 123 }.map { - 456 - }.value! - - assert GitHub::Data::Result.new { raise "nope" }.map { - flunk "should not have invoked map block" - }.error - end - - def test_value - assert_equal 123, GitHub::Data::Result.new { 123 }.value { 456 } - - assert_equal 456, GitHub::Data::Result.new { raise "nope" }.value { 456 } - end - - def test_value! - assert_equal 123, GitHub::Data::Result.new { 123 }.value! - - r = GitHub::Data::Result.new { raise "nope" } - - assert_raises RuntimeError do - r.value! - end - end - - def test_ok? - assert_predicate GitHub::Data::Result.new { 123 }, :ok? - - refute_predicate GitHub::Data::Result.new { raise "nope" }, :ok? - end - - def test_error - assert_nil GitHub::Data::Result.new { 123 }.error - - e = StandardError.new("nope") - - assert_equal e, GitHub::Data::Result.new { raise e }.error - end -end diff --git a/test/github/data_test.rb b/test/github/data_test.rb deleted file mode 100644 index 5f7cc1d..0000000 --- a/test/github/data_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class Github::DataTest < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::Github::Data::VERSION - end -end diff --git a/test/github/kv/result_test.rb b/test/github/kv/result_test.rb new file mode 100644 index 0000000..f1190f0 --- /dev/null +++ b/test/github/kv/result_test.rb @@ -0,0 +1,86 @@ +require 'test_helper' + +class GitHub::KV::ResultTest < Minitest::Test + def test_to_s + assert_match %r{#}, GitHub::KV::Result.new { 123 }.to_s + + assert_match %r{#>}, GitHub::KV::Result.new { raise "nope" }.to_s + end + + def test_then + assert_equal 456, GitHub::KV::Result.new { 123 }.then { + GitHub::KV::Result.new { 456 } + }.value! + + assert GitHub::KV::Result.new { raise "nope" }.then { + flunk "should not have invoked then block" + }.error + + assert_raises TypeError do + GitHub::KV::Result.new {}.then { + "not a result" + } + end + end + + def test_rescue + assert_equal 456, GitHub::KV::Result.new { raise "nope" }.rescue { + GitHub::KV::Result.new { 456 } + }.value! + + assert_equal 456, GitHub::KV::Result.new { raise "nope" }.rescue { |error| + assert_equal "nope", error.message + GitHub::KV::Result.new { 456 } + }.value! + + assert GitHub::KV::Result.new { 123 }.rescue { + flunk "should not have invoked rescue block" + }.value! + + assert_raises TypeError do + GitHub::KV::Result.new { raise "nope" }.rescue { + "not a result" + } + end + end + + def test_map + assert_equal 456, GitHub::KV::Result.new { 123 }.map { + 456 + }.value! + + assert GitHub::KV::Result.new { raise "nope" }.map { + flunk "should not have invoked map block" + }.error + end + + def test_value + assert_equal 123, GitHub::KV::Result.new { 123 }.value { 456 } + + assert_equal 456, GitHub::KV::Result.new { raise "nope" }.value { 456 } + end + + def test_value! + assert_equal 123, GitHub::KV::Result.new { 123 }.value! + + r = GitHub::KV::Result.new { raise "nope" } + + assert_raises RuntimeError do + r.value! + end + end + + def test_ok? + assert_predicate GitHub::KV::Result.new { 123 }, :ok? + + refute_predicate GitHub::KV::Result.new { raise "nope" }, :ok? + end + + def test_error + assert_nil GitHub::KV::Result.new { 123 }.error + + e = StandardError.new("nope") + + assert_equal e, GitHub::KV::Result.new { raise e }.error + end +end diff --git a/test/github/data/sql_test.rb b/test/github/kv/sql_test.rb similarity index 62% rename from test/github/data/sql_test.rb rename to test/github/kv/sql_test.rb index ab470e6..cc2057b 100644 --- a/test/github/data/sql_test.rb +++ b/test/github/kv/sql_test.rb @@ -1,13 +1,13 @@ require "test_helper" -class GitHub::Data::SQLTest < Minitest::Test +class GitHub::KV::SQLTest < Minitest::Test local_time = Time.utc(1970, 1, 1, 0, 0, 0) Timecop.freeze(local_time) do - foo = GitHub::Data::SQL::LITERAL "foo" - rows = GitHub::Data::SQL::ROWS [[1, 2], [3, 4]] + foo = GitHub::KV::SQL::LITERAL "foo" + rows = GitHub::KV::SQL::ROWS [[1, 2], [3, 4]] SANITIZE_TESTS = [ - [GitHub::Data::SQL, "'GitHub::Data::SQL'"], + [GitHub::KV::SQL, "'GitHub::KV::SQL'"], [DateTime.now.utc, "'1970-01-01 00:00:00'"], [Time.now.utc, "'1970-01-01 00:00:00'"], [Time.now.utc.to_date, "'1970-01-01'"], @@ -34,22 +34,22 @@ class GitHub::Data::SQLTest < Minitest::Test def test_sanitize SANITIZE_TESTS.each do |input, expected| - assert_equal expected, GitHub::Data::SQL.new.sanitize(input), + assert_equal expected, GitHub::KV::SQL.new.sanitize(input), "#{input.inspect} sanitizes as #{expected.inspect}" end end def test_sanitize_bad_values BAD_VALUE_TESTS.each do |input| - assert_raises GitHub::Data::SQL::BadValue, "#{input.inspect} (#{input.class}) raises BadValue when sanitized" do - GitHub::Data::SQL.new.sanitize input + assert_raises GitHub::KV::SQL::BadValue, "#{input.inspect} (#{input.class}) raises BadValue when sanitized" do + GitHub::KV::SQL.new.sanitize input end end end def test_initialize_with_query str = "query" - sql = GitHub::Data::SQL.new str + sql = GitHub::KV::SQL.new str assert_equal Hash.new, sql.binds assert_equal str, sql.query @@ -58,7 +58,7 @@ class GitHub::Data::SQLTest < Minitest::Test def test_initialize_with_binds binds = { :key => "value" } - sql = GitHub::Data::SQL.new binds + sql = GitHub::KV::SQL.new binds assert_equal "", sql.query assert_equal "value", sql.binds[:key] @@ -66,49 +66,49 @@ class GitHub::Data::SQLTest < Minitest::Test end def test_initialize_with_query_and_binds - sql = GitHub::Data::SQL.new "query :key", :key => "value" + sql = GitHub::KV::SQL.new "query :key", :key => "value" assert_equal "query 'value'", sql.query assert_equal "value", sql.binds[:key] end def test_initialize_with_single_character_binds - sql = GitHub::Data::SQL.new "query :x", :x => "y" + sql = GitHub::KV::SQL.new "query :x", :x => "y" assert_equal "query 'y'", sql.query assert_equal "y", sql.binds[:x] end def test_add - sql = GitHub::Data::SQL.new + sql = GitHub::KV::SQL.new sql.add("first").add "second" assert_equal "first second", sql.query end def test_add_with_binds - sql = GitHub::Data::SQL.new + sql = GitHub::KV::SQL.new sql.add ":local", :local => "value" assert_equal "'value'", sql.query - assert_raises GitHub::Data::SQL::BadBind do + assert_raises GitHub::KV::SQL::BadBind do sql.add ":local" # the previous value doesn't persist end end def test_add_with_leading_and_trailing_whitespace - sql = GitHub::Data::SQL.new " query " + sql = GitHub::KV::SQL.new " query " assert_equal "query", sql.query end def test_add_date now = Time.now.utc - sql = GitHub::Data::SQL.new ":now", :now => now + sql = GitHub::KV::SQL.new ":now", :now => now assert_equal "'#{now.to_s(:db)}'", sql.query end def test_bind - sql = GitHub::Data::SQL.new + sql = GitHub::KV::SQL.new sql.bind(:first => "firstval").bind(:second => "secondval") assert_equal "firstval", sql.binds[:first] @@ -116,7 +116,7 @@ class GitHub::Data::SQLTest < Minitest::Test end def test_initialize_with_connection - sql = GitHub::Data::SQL.new :connection => "stub" + sql = GitHub::KV::SQL.new :connection => "stub" assert_equal "stub", sql.connection assert_nil sql.binds[:connection] @@ -126,65 +126,65 @@ class GitHub::Data::SQLTest < Minitest::Test first, second = nil ActiveRecord::Base.cache do - first = GitHub::Data::SQL.new("SELECT RAND()").value - second = GitHub::Data::SQL.new("SELECT RAND()").value + first = GitHub::KV::SQL.new("SELECT RAND()").value + second = GitHub::KV::SQL.new("SELECT RAND()").value end assert_in_delta first, second end def test_add_unless_empty_adds_to_a_non_empty_query - sql = GitHub::Data::SQL.new "non-empty" + sql = GitHub::KV::SQL.new "non-empty" sql.add_unless_empty "foo" assert_includes sql.query, "foo" end def test_add_unless_empty_does_not_add_to_an_empty_query - sql = GitHub::Data::SQL.new + sql = GitHub::KV::SQL.new sql.add_unless_empty "foo" refute_includes sql.query, "foo" end def test_literal - assert_kind_of GitHub::Data::SQL::Literal, GitHub::Data::SQL::LITERAL("foo") + assert_kind_of GitHub::KV::SQL::Literal, GitHub::KV::SQL::LITERAL("foo") end def test_rows - assert_kind_of GitHub::Data::SQL::Rows, GitHub::Data::SQL::ROWS([[1, 2, 3], [4, 5, 6]]) + assert_kind_of GitHub::KV::SQL::Rows, GitHub::KV::SQL::ROWS([[1, 2, 3], [4, 5, 6]]) end def test_rows_raises_if_non_arrays_are_provided assert_raises(ArgumentError) do - GitHub::Data::SQL::ROWS([1, 2, 3]) + GitHub::KV::SQL::ROWS([1, 2, 3]) end end def test_affected_rows begin - GitHub::Data::SQL.run("CREATE TEMPORARY TABLE affected_rows_test (x INT)") - GitHub::Data::SQL.run("INSERT INTO affected_rows_test VALUES (1), (2), (3), (4)") + GitHub::KV::SQL.run("CREATE TEMPORARY TABLE affected_rows_test (x INT)") + GitHub::KV::SQL.run("INSERT INTO affected_rows_test VALUES (1), (2), (3), (4)") - sql = GitHub::Data::SQL.new("UPDATE affected_rows_test SET x = x + 1") + sql = GitHub::KV::SQL.new("UPDATE affected_rows_test SET x = x + 1") sql.run assert_equal 4, sql.affected_rows ensure - GitHub::Data::SQL.run("DROP TABLE affected_rows_test") + GitHub::KV::SQL.run("DROP TABLE affected_rows_test") end end def test_affected_rows_even_when_query_generates_warning begin - GitHub::Data::SQL.run("CREATE TEMPORARY TABLE affected_rows_test (x INT)") - GitHub::Data::SQL.run("INSERT INTO affected_rows_test VALUES (1), (2), (3), (4)") - sql = GitHub::Data::SQL.new("UPDATE affected_rows_test SET x = x + 1 WHERE 1 = '1x'") + GitHub::KV::SQL.run("CREATE TEMPORARY TABLE affected_rows_test (x INT)") + GitHub::KV::SQL.run("INSERT INTO affected_rows_test VALUES (1), (2), (3), (4)") + sql = GitHub::KV::SQL.new("UPDATE affected_rows_test SET x = x + 1 WHERE 1 = '1x'") sql.run assert_equal 4, sql.affected_rows ensure - GitHub::Data::SQL.run("DROP TABLE affected_rows_test") + GitHub::KV::SQL.run("DROP TABLE affected_rows_test") end end end diff --git a/test/github/data/kv_test.rb b/test/github/kv_test.rb similarity index 84% rename from test/github/data/kv_test.rb rename to test/github/kv_test.rb index 79c0767..0de2a77 100644 --- a/test/github/data/kv_test.rb +++ b/test/github/kv_test.rb @@ -1,14 +1,14 @@ require "test_helper" -class GitHub::Data::KVTest < Minitest::Test +class GitHub::KVTest < Minitest::Test def setup ActiveRecord::Base.connection.execute("TRUNCATE `key_values`") - @kv = GitHub::Data::KV.new { ActiveRecord::Base.connection } + @kv = GitHub::KV.new { ActiveRecord::Base.connection } end def test_initialize_without_connection - kv = GitHub::Data::KV.new - assert_raises GitHub::Data::KV::MissingConnectionError do + kv = GitHub::KV.new + assert_raises GitHub::KV::MissingConnectionError do kv.get("foo").value! end end @@ -41,7 +41,7 @@ class GitHub::Data::KVTest < Minitest::Test def test_set_failure ActiveRecord::Base.connection.stubs(:insert).raises(Errno::ECONNRESET) - assert_raises GitHub::Data::KV::UnavailableError do + assert_raises GitHub::KV::UnavailableError do @kv.set("foo", "bar") end end @@ -71,7 +71,7 @@ class GitHub::Data::KVTest < Minitest::Test def test_setnx_failure ActiveRecord::Base.connection.stubs(:delete).raises(Errno::ECONNRESET) - assert_raises GitHub::Data::KV::UnavailableError do + assert_raises GitHub::KV::UnavailableError do @kv.setnx("foo", "bar") end end @@ -86,7 +86,7 @@ class GitHub::Data::KVTest < Minitest::Test def test_del_failure ActiveRecord::Base.connection.stubs(:delete).raises(Errno::ECONNRESET) - assert_raises GitHub::Data::KV::UnavailableError do + assert_raises GitHub::KV::UnavailableError do @kv.del("foo") end end @@ -106,7 +106,7 @@ class GitHub::Data::KVTest < Minitest::Test @kv.set("foo", "bar", expires: expires) - assert_equal expires, GitHub::Data::SQL.value(<<-SQL) + assert_equal expires, GitHub::KV::SQL.value(<<-SQL) SELECT expires_at FROM key_values WHERE `key` = 'foo' SQL end @@ -116,7 +116,7 @@ class GitHub::Data::KVTest < Minitest::Test @kv.setnx("foo", "bar", expires: expires) - assert_equal expires, GitHub::Data::SQL.value(<<-SQL) + assert_equal expires, GitHub::KV::SQL.value(<<-SQL) SELECT expires_at FROM key_values WHERE `key` = 'foo' SQL end @@ -145,7 +145,7 @@ class GitHub::Data::KVTest < Minitest::Test @kv.set("foo", "bar", expires: 1.hour.from_now) @kv.set("foo", "bar") - assert_nil GitHub::Data::SQL.value(<<-SQL) + assert_nil GitHub::KV::SQL.value(<<-SQL) SELECT expires_at FROM key_values WHERE `key` = "foo" SQL end @@ -165,7 +165,7 @@ class GitHub::Data::KVTest < Minitest::Test end def test_length_checks_key - assert_raises GitHub::Data::KV::KeyLengthError do + assert_raises GitHub::KV::KeyLengthError do @kv.get("A" * 256) end end @@ -177,7 +177,7 @@ class GitHub::Data::KVTest < Minitest::Test end def test_length_checks_value - assert_raises GitHub::Data::KV::ValueLengthError do + assert_raises GitHub::KV::ValueLengthError do @kv.set("foo", "A" * 65536) end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 6024484..bd63c7e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,6 @@ require "bundler/setup" $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) -require "github/data" +require "github/kv" require "timecop" require "minitest/autorun"