Rename from generic github-data to specific github-kv

This commit is contained in:
John Nunemaker 2017-03-16 16:17:17 -04:00
Родитель 5904050f4a
Коммит c3fb2faeb3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: D80B5604D6DC62E0
22 изменённых файлов: 573 добавлений и 591 удалений

Просмотреть файл

@ -1,7 +1,7 @@
## Contributing ## Contributing
[fork]: https://github.com/github/github-data/fork [fork]: https://github.com/github/github-kv/fork
[pr]: https://github.com/github/github-data/compare [pr]: https://github.com/github/github-kv/compare
[style]: https://github.com/styleguide/ruby [style]: https://github.com/styleguide/ruby
[code-of-conduct]: CODE_OF_CONDUCT.md [code-of-conduct]: CODE_OF_CONDUCT.md

Просмотреть файл

@ -1,6 +1,6 @@
source "https://rubygems.org" source "https://rubygems.org"
# Specify your gem's dependencies in github-data.gemspec # Specify your gem's dependencies in github-kv.gemspec
gemspec gemspec
gem "rails", "~> #{ENV['RAILS_VERSION'] || '5.0.2'}" gem "rails", "~> #{ENV['RAILS_VERSION'] || '5.0.2'}"

Просмотреть файл

@ -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::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::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::KV::Result` makes it easier to bake in resiliency through the use of a Result object instead of raising exceptions.
* `GitHub::Data::Result` makes it easier to bake in resiliency through the use of a Result object instead of raising exceptions.
## Installation ## Installation
Add this line to your application's Gemfile: Add this line to your application's Gemfile:
```ruby ```ruby
gem 'github-data' gem 'github-kv'
``` ```
And then execute: And then execute:
@ -20,16 +19,16 @@ And then execute:
Or install it yourself as: Or install it yourself as:
$ gem install github-data $ gem install github-kv
## Usage ## 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 rails db:migrate
``` ```
@ -37,14 +36,14 @@ Once you have the table, KV can do neat things like this:
```ruby ```ruby
require "pp" require "pp"
require "github/data/kv" require "github/kv"
# Create new instance using ActiveRecord's default connection. # 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. # Get a key.
pp kv.get("foo") pp kv.get("foo")
#<GitHub::Data::Result:0x3fd88cd3ea9c value: nil> #<GitHub::KV::Result:0x3fd88cd3ea9c value: nil>
# Set a key. # Set a key.
kv.set("foo", "bar") kv.set("foo", "bar")
@ -52,23 +51,23 @@ kv.set("foo", "bar")
# Get the key again. # Get the key again.
pp kv.get("foo") pp kv.get("foo")
#<GitHub::Data::Result:0x3fe810d06e4c value: "bar"> #<GitHub::KV::Result:0x3fe810d06e4c value: "bar">
# Get multiple keys at once. # Get multiple keys at once.
pp kv.mget(["foo", "bar"]) pp kv.mget(["foo", "bar"])
#<GitHub::Data::Result:0x3fccccd1b57c value: ["bar", nil]> #<GitHub::KV::Result:0x3fccccd1b57c value: ["bar", nil]>
# Check for existence of a key. # Check for existence of a key.
pp kv.exists("foo") pp kv.exists("foo")
#<GitHub::Data::Result:0x3fd4ae55ce8c value: true> #<GitHub::KV::Result:0x3fd4ae55ce8c value: true>
# Check for existence of key that does not exist. # Check for existence of key that does not exist.
pp kv.exists("bar") pp kv.exists("bar")
#<GitHub::Data::Result:0x3fd4ae55c554 value: false> #<GitHub::KV::Result:0x3fd4ae55c554 value: false>
# Check for existence of multiple keys at once. # Check for existence of multiple keys at once.
pp kv.mexists(["foo", "bar"]) pp kv.mexists(["foo", "bar"])
#<GitHub::Data::Result:0x3ff1e98e18e8 value: [true, false]> #<GitHub::KV::Result:0x3ff1e98e18e8 value: [true, false]>
# Set a key's value if the key does not already exist. # Set a key's value if the key does not already exist.
pp kv.setnx("foo", "bar") pp kv.setnx("foo", "bar")
@ -91,7 +90,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
## Contributing ## 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 ## License

Просмотреть файл

@ -9,7 +9,7 @@ require "active_record"
ActiveRecord::Base.establish_connection({ ActiveRecord::Base.establish_connection({
adapter: "mysql2", adapter: "mysql2",
database: "github_data_test", database: "github_kv_test",
}) })
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS `key_values`") ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS `key_values`")

Просмотреть файл

@ -1,12 +1,12 @@
require File.expand_path("../example_setup", __FILE__) require File.expand_path("../example_setup", __FILE__)
require "github/data/kv" require "github/kv"
# Create new instance using ActiveRecord's default connection. # 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. # Get a key.
pp kv.get("foo") pp kv.get("foo")
#<GitHub::Data::Result:0x3fd88cd3ea9c value: nil> #<GitHub::KV::Result:0x3fd88cd3ea9c value: nil>
# Set a key. # Set a key.
kv.set("foo", "bar") kv.set("foo", "bar")
@ -14,23 +14,23 @@ kv.set("foo", "bar")
# Get the key again. # Get the key again.
pp kv.get("foo") pp kv.get("foo")
#<GitHub::Data::Result:0x3fe810d06e4c value: "bar"> #<GitHub::KV::Result:0x3fe810d06e4c value: "bar">
# Get multiple keys at once. # Get multiple keys at once.
pp kv.mget(["foo", "bar"]) pp kv.mget(["foo", "bar"])
#<GitHub::Data::Result:0x3fccccd1b57c value: ["bar", nil]> #<GitHub::KV::Result:0x3fccccd1b57c value: ["bar", nil]>
# Check for existence of a key. # Check for existence of a key.
pp kv.exists("foo") pp kv.exists("foo")
#<GitHub::Data::Result:0x3fd4ae55ce8c value: true> #<GitHub::KV::Result:0x3fd4ae55ce8c value: true>
# Check for existence of key that does not exist. # Check for existence of key that does not exist.
pp kv.exists("bar") pp kv.exists("bar")
#<GitHub::Data::Result:0x3fd4ae55c554 value: false> #<GitHub::KV::Result:0x3fd4ae55c554 value: false>
# Check for existence of multiple keys at once. # Check for existence of multiple keys at once.
pp kv.mexists(["foo", "bar"]) pp kv.mexists(["foo", "bar"])
#<GitHub::Data::Result:0x3ff1e98e18e8 value: [true, false]> #<GitHub::KV::Result:0x3ff1e98e18e8 value: [true, false]>
# Set a key's value if the key does not already exist. # Set a key's value if the key does not already exist.
pp kv.setnx("foo", "bar") pp kv.setnx("foo", "bar")

Просмотреть файл

@ -1,23 +1,23 @@
# coding: utf-8 # coding: utf-8
lib = File.expand_path('../lib', __FILE__) lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'github/data/version' require "github/kv/version"
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "github-data" spec.name = "github-kv"
spec.version = Github::Data::VERSION spec.version = Github::KV::VERSION
spec.authors = ["GitHub Open Source", "John Nunemaker"] 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.summary = %q{A key/value data store backed by MySQL.}
spec.description = %q{Useful tools for working with SQL data.} spec.description = %q{A key/value data store backed by MySQL.}
spec.homepage = "https://github.com/github/github-data" spec.homepage = "https://github.com/github/github-kv"
spec.license = "MIT" spec.license = "MIT"
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' # 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. # to allow pushing to a single host or delete this section to allow pushing to any host.
if spec.respond_to?(:metadata) if spec.respond_to?(:metadata)
spec.metadata['allowed_push_host'] = "https://rubygems.org" spec.metadata["allowed_push_host"] = "https://rubygems.org"
else else
raise "RubyGems 2.0 or newer is required to protect against " \ raise "RubyGems 2.0 or newer is required to protect against " \
"public gem pushes." "public gem pushes."

Просмотреть файл

@ -1,7 +1,7 @@
require 'rails/generators/active_record' require 'rails/generators/active_record'
module Github module Github
module Data class KV
module Generators module Generators
class ActiveRecordGenerator < ::Rails::Generators::Base class ActiveRecordGenerator < ::Rails::Generators::Base
include ::Rails::Generators::Migration include ::Rails::Generators::Migration

Просмотреть файл

@ -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

Просмотреть файл

@ -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<String | nil>
#
# Gets the value of the specified key.
#
# Example:
#
# kv.get("foo")
# # => #<Result value: "bar">
#
# kv.get("octocat")
# # => #<Result value: nil>
#
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"])
# # => #<Result value: ["bar", nil]
#
def mget(keys)
validate_key_array(keys)
Result.new {
kvs = GitHub::Data::SQL.results(<<-SQL, :keys => 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<Boolean>
#
# Checks for existence of the specified key.
#
# Example:
#
# kv.exists("foo")
# # => #<Result value: true>
#
# kv.exists("octocat")
# # => #<Result value: false>
#
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"])
# # => #<Result value: [true, false]>
#
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

344
lib/github/kv.rb Normal file
Просмотреть файл

@ -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<String | nil>
#
# Gets the value of the specified key.
#
# Example:
#
# kv.get("foo")
# # => #<Result value: "bar">
#
# kv.get("octocat")
# # => #<Result value: nil>
#
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"])
# # => #<Result value: ["bar", nil]
#
def mget(keys)
validate_key_array(keys)
Result.new {
kvs = GitHub::KV::SQL.results(<<-SQL, :keys => 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<Boolean>
#
# Checks for existence of the specified key.
#
# Example:
#
# kv.exists("foo")
# # => #<Result value: true>
#
# kv.exists("octocat")
# # => #<Result value: false>
#
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"])
# # => #<Result value: [true, false]>
#
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

Просмотреть файл

@ -1,18 +1,18 @@
module GitHub module GitHub
module Data class KV
class Result class Result
# Invokes the supplied block and wraps the return value in a # 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. # Exceptions raised by the block are caught and also wrapped.
# #
# Example: # Example:
# #
# GitHub::Data::Result.new { 123 } # GitHub::KV::Result.new { 123 }
# # => #<GitHub::Data::Result value: 123> # # => #<GitHub::KV::Result value: 123>
# #
# GitHub::Data::Result.new { raise "oops" } # GitHub::KV::Result.new { raise "oops" }
# # => #<GitHub::Data::Result error: #<RuntimeError: oops>> # # => #<GitHub::KV::Result error: #<RuntimeError: oops>>
# #
def initialize def initialize
begin begin
@ -25,9 +25,9 @@ module GitHub
def to_s def to_s
if ok? if ok?
"#<GitHub::Data::Result:0x%x value: %s>" % [object_id, @value.inspect] "#<GitHub::KV::Result:0x%x value: %s>" % [object_id, @value.inspect]
else else
"#<GitHub::Data::Result:0x%x error: %s>" % [object_id, @error.inspect] "#<GitHub::KV::Result:0x%x error: %s>" % [object_id, @error.inspect]
end end
end end
@ -38,7 +38,7 @@ module GitHub
# #
# If the result represents an error, returns self. # 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. # Use #map otherwise.
# #
# Example: # Example:
@ -46,17 +46,17 @@ module GitHub
# result = do_something().then { |val| # result = do_something().then { |val|
# do_other_thing(val) # do_other_thing(val)
# } # }
# # => #<GitHub::Data::Result value: ...> # # => #<GitHub::KV::Result value: ...>
# #
# do_something_that_fails().then { |val| # do_something_that_fails().then { |val|
# # never invoked # # never invoked
# } # }
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
def then def then
if ok? if ok?
result = yield(@value) 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 result
else else
self self
@ -67,7 +67,7 @@ module GitHub
# #
# If the result represents a value, returns self. # 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. # Use #map otherwise.
# #
# Example: # Example:
@ -75,41 +75,41 @@ module GitHub
# result = do_something().rescue { |val| # result = do_something().rescue { |val|
# # never invoked # # never invoked
# } # }
# # => #<GitHub::Data::Result value: ...> # # => #<GitHub::KV::Result value: ...>
# #
# do_something_that_fails().rescue { |val| # do_something_that_fails().rescue { |val|
# # handle_error(val) # # handle_error(val)
# } # }
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
def rescue def rescue
return self if ok? return self if ok?
result = yield(@error) 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 result
end end
# If the result represents a value, invokes the supplied block with that # 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. # If the result represents an error, returns self.
# #
# The block should not return a GitHub::Data::Result object (unless you # The block should not return a GitHub::KV::Result object (unless you
# truly intend to create a GitHub::Data::Result<GitHub::Data::Result<T>>). # truly intend to create a GitHub::KV::Result<GitHub::KV::Result<T>>).
# Use #then if it does. # Use #then if it does.
# #
# Example: # Example:
# #
# result = do_something() # result = do_something()
# # => #<GitHub::Data::Result value: 123> # # => #<GitHub::KV::Result value: 123>
# #
# result.map { |val| val * 2 } # result.map { |val| val * 2 }
# # => #<GitHub::Data::Result value: 246> # # => #<GitHub::KV::Result value: 246>
# #
# do_something_that_fails().map { |val| # do_something_that_fails().map { |val|
# # never invoked # # never invoked
# } # }
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
def map def map
if ok? if ok?
@ -127,20 +127,20 @@ module GitHub
# Example: # Example:
# #
# result = do_something() # result = do_something()
# # => #<GitHub::Data::Result value: "foo"> # # => #<GitHub::KV::Result value: "foo">
# #
# result.value { "nope" } # result.value { "nope" }
# # => "foo" # # => "foo"
# #
# result = do_something_that_fails() # result = do_something_that_fails()
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
# result.value { "nope" } # result.value { "nope" }
# # => #<GitHub::Data::Result value: "nope"> # # => #<GitHub::KV::Result value: "nope">
# #
def value def value
unless block_given? 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 end
if ok? if ok?
@ -157,13 +157,13 @@ module GitHub
# Example: # Example:
# #
# result = do_something() # result = do_something()
# # => #<GitHub::Data::Result value: "foo"> # # => #<GitHub::KV::Result value: "foo">
# #
# result.value! # result.value!
# # => "foo" # # => "foo"
# #
# result = do_something_that_fails() # result = do_something_that_fails()
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
# result.value! # result.value!
# # !! raises exception # # !! raises exception
@ -181,13 +181,13 @@ module GitHub
# Example: # Example:
# #
# result = do_something() # result = do_something()
# # => #<GitHub::Data::Result value: "foo"> # # => #<GitHub::KV::Result value: "foo">
# #
# result.ok? # result.ok?
# # => true # # => true
# #
# result = do_something_that_fails() # result = do_something_that_fails()
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
# result.ok? # result.ok?
# # => false # # => false
@ -201,13 +201,13 @@ module GitHub
# If the result represents an error, returns that error. # If the result represents an error, returns that error.
# #
# result = do_something() # result = do_something()
# # => #<GitHub::Data::Result value: "foo"> # # => #<GitHub::KV::Result value: "foo">
# #
# result.error # result.error
# # => nil # # => nil
# #
# result = do_something_that_fails() # result = do_something_that_fails()
# # => #<GitHub::Data::Result error: ...> # # => #<GitHub::KV::Result error: ...>
# #
# result.error # result.error
# # => ... # # => ...
@ -216,10 +216,10 @@ module GitHub
@error @error
end 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)
# # => # <GitHub::Data::Result error: ...> # # => # <GitHub::KV::Result error: ...>
# #
def self.error(e) def self.error(e)
result = allocate result = allocate

Просмотреть файл

@ -2,14 +2,14 @@ require "active_record"
require "active_support/all" require "active_support/all"
module GitHub module GitHub
module Data class KV
# Public: Build and execute a SQL query, returning results as Arrays. This # Public: Build and execute a SQL query, returning results as Arrays. This
# class uses ActiveRecord's connection classes, but provides a better API for # class uses ActiveRecord's connection classes, but provides a better API for
# bind values and raw data access. # bind values and raw data access.
# #
# Example: # 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 # SELECT * FROM repositories
# WHERE source_id = :network_id AND parent_id IN :parent_ids # WHERE source_id = :network_id AND parent_id IN :parent_ids
# SQL # SQL
@ -30,7 +30,7 @@ module GitHub
# #
# * Arrays are escaped as `(item, item, item)`. If you need to insert multiple # * Arrays are escaped as `(item, item, item)`. If you need to insert multiple
# rows (Arrays of Arrays), you must specify the bind value using # 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 class SQL
@ -78,7 +78,7 @@ module GitHub
# Used when a column contains binary data which needs to be escaped # Used when a column contains binary data which needs to be escaped
# to prevent warnings from MySQL # to prevent warnings from MySQL
def self.BINARY(string) 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 end
# Public: Escape a binary SQL value, yielding a string which can be used as # 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. # aren't available to subsequent adds.
# #
# Returns self. # 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) def add(sql, extras = nil)
return self if sql.blank? return self if sql.blank?
@ -186,7 +186,7 @@ module GitHub
# aren't available to subsequent adds. # aren't available to subsequent adds.
# #
# Returns self. # 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) def add_unless_empty(sql, extras = nil)
return self if query.empty? return self if query.empty?
add sql, extras add sql, extras
@ -315,8 +315,8 @@ module GitHub
# Public: Execute, ignoring results. This is useful when the results of a # Public: Execute, ignoring results. This is useful when the results of a
# query aren't important, often INSERTs, UPDATEs, or DELETEs. # query aren't important, often INSERTs, UPDATEs, or DELETEs.
# #
# sql - An optional SQL string. 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::Data::SQL#add for details. # extras - Optional bind values. See GitHub::KV::SQL#add for details.
# #
# Returns self. # Returns self.
def run(sql = nil, extras = nil) def run(sql = nil, extras = nil)
@ -337,8 +337,8 @@ module GitHub
# Public: Create and execute a new SQL query, ignoring results. # Public: Create and execute a new SQL query, ignoring results.
# #
# sql - A SQL string. See GitHub::Data::SQL#add for details. # sql - A SQL string. See GitHub::KV::SQL#add for details.
# bindings - Optional bind values. See GitHub::Data::SQL#add for details. # bindings - Optional bind values. See GitHub::KV::SQL#add for details.
# #
# Returns self. # Returns self.
def self.run(sql, bindings = {}) def self.run(sql, bindings = {})
@ -347,8 +347,8 @@ module GitHub
# Public: Create and execute a new SQL query, returning its hash_result rows. # Public: Create and execute a new SQL query, returning its hash_result rows.
# #
# sql - A SQL string. See GitHub::Data::SQL#add for details. # sql - A SQL string. See GitHub::KV::SQL#add for details.
# bindings - Optional bind values. See GitHub::Data::SQL#add for details. # bindings - Optional bind values. See GitHub::KV::SQL#add for details.
# #
# Returns an Array of result hashes. # Returns an Array of result hashes.
def self.hash_results(sql, bindings = {}) def self.hash_results(sql, bindings = {})
@ -357,8 +357,8 @@ module GitHub
# Public: Create and execute a new SQL query, returning its result rows. # Public: Create and execute a new SQL query, returning its result rows.
# #
# sql - A SQL string. See GitHub::Data::SQL#add for details. # sql - A SQL string. See GitHub::KV::SQL#add for details.
# bindings - Optional bind values. See GitHub::Data::SQL#add for details. # bindings - Optional bind values. See GitHub::KV::SQL#add for details.
# #
# Returns an Array of result arrays. # Returns an Array of result arrays.
def self.results(sql, bindings = {}) def self.results(sql, bindings = {})
@ -368,8 +368,8 @@ module GitHub
# Public: Create and execute a new SQL query, returning the value of the # Public: Create and execute a new SQL query, returning the value of the
# first column of the first result row. # first column of the first result row.
# #
# sql - A SQL string. See GitHub::Data::SQL#add for details. # sql - A SQL string. See GitHub::KV::SQL#add for details.
# bindings - Optional bind values. See GitHub::Data::SQL#add for details. # bindings - Optional bind values. See GitHub::KV::SQL#add for details.
# #
# Returns a value or nil. # Returns a value or nil.
def self.value(sql, bindings = {}) def self.value(sql, bindings = {})
@ -378,8 +378,8 @@ module GitHub
# Public: Create and execute a new SQL query, returning its values. # Public: Create and execute a new SQL query, returning its values.
# #
# sql - A SQL string. See GitHub::Data::SQL#add for details. # sql - A SQL string. See GitHub::KV::SQL#add for details.
# bindings - Optional bind values. See GitHub::Data::SQL#add for details. # bindings - Optional bind values. See GitHub::KV::SQL#add for details.
# #
# Returns an Array of values. # Returns an Array of values.
def self.values(sql, bindings = {}) def self.values(sql, bindings = {})

Просмотреть файл

@ -1,5 +1,5 @@
module Github module Github
module Data class KV
VERSION = "0.1.0" VERSION = "0.1.0"
end end
end end

Просмотреть файл

@ -1,7 +1,7 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
require "bundler/setup" require "bundler/setup"
require "github/data" require "github/kv"
# You can add fixtures and/or initialization code here to make experimenting # 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. # with your gem easier. You can also use a different console, if you like.

Просмотреть файл

@ -3,10 +3,10 @@ require "rails"
require "rails/test_help" require "rails/test_help"
require "active_record" require "active_record"
require "rails/generators/test_case" require "rails/generators/test_case"
require "generators/github/data/active_record_generator" require "generators/github/kv/active_record_generator"
class GitHubDataActiveRecordGeneratorTest < Rails::Generators::TestCase class GithubKVActiveRecordGeneratorTest < Rails::Generators::TestCase
tests Github::Data::Generators::ActiveRecordGenerator tests Github::KV::Generators::ActiveRecordGenerator
destination File.expand_path("../../../../tmp", __FILE__) destination File.expand_path("../../../../tmp", __FILE__)
setup :prepare_destination setup :prepare_destination

Просмотреть файл

@ -1,86 +0,0 @@
require 'test_helper'
class GitHub::Data::ResultTest < Minitest::Test
def test_to_s
assert_match %r{#<GitHub::Data::Result:0x[a-f0-9]+ value: 123>}, GitHub::Data::Result.new { 123 }.to_s
assert_match %r{#<GitHub::Data::Result:0x[a-f0-9]+ error: #<RuntimeError: nope>>}, 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

Просмотреть файл

@ -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

Просмотреть файл

@ -0,0 +1,86 @@
require 'test_helper'
class GitHub::KV::ResultTest < Minitest::Test
def test_to_s
assert_match %r{#<GitHub::KV::Result:0x[a-f0-9]+ value: 123>}, GitHub::KV::Result.new { 123 }.to_s
assert_match %r{#<GitHub::KV::Result:0x[a-f0-9]+ error: #<RuntimeError: nope>>}, 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

Просмотреть файл

@ -1,13 +1,13 @@
require "test_helper" 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) local_time = Time.utc(1970, 1, 1, 0, 0, 0)
Timecop.freeze(local_time) do Timecop.freeze(local_time) do
foo = GitHub::Data::SQL::LITERAL "foo" foo = GitHub::KV::SQL::LITERAL "foo"
rows = GitHub::Data::SQL::ROWS [[1, 2], [3, 4]] rows = GitHub::KV::SQL::ROWS [[1, 2], [3, 4]]
SANITIZE_TESTS = [ SANITIZE_TESTS = [
[GitHub::Data::SQL, "'GitHub::Data::SQL'"], [GitHub::KV::SQL, "'GitHub::KV::SQL'"],
[DateTime.now.utc, "'1970-01-01 00:00:00'"], [DateTime.now.utc, "'1970-01-01 00:00:00'"],
[Time.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'"], [Time.now.utc.to_date, "'1970-01-01'"],
@ -34,22 +34,22 @@ class GitHub::Data::SQLTest < Minitest::Test
def test_sanitize def test_sanitize
SANITIZE_TESTS.each do |input, expected| 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}" "#{input.inspect} sanitizes as #{expected.inspect}"
end end
end end
def test_sanitize_bad_values def test_sanitize_bad_values
BAD_VALUE_TESTS.each do |input| BAD_VALUE_TESTS.each do |input|
assert_raises GitHub::Data::SQL::BadValue, "#{input.inspect} (#{input.class}) raises BadValue when sanitized" do assert_raises GitHub::KV::SQL::BadValue, "#{input.inspect} (#{input.class}) raises BadValue when sanitized" do
GitHub::Data::SQL.new.sanitize input GitHub::KV::SQL.new.sanitize input
end end
end end
end end
def test_initialize_with_query def test_initialize_with_query
str = "query" str = "query"
sql = GitHub::Data::SQL.new str sql = GitHub::KV::SQL.new str
assert_equal Hash.new, sql.binds assert_equal Hash.new, sql.binds
assert_equal str, sql.query assert_equal str, sql.query
@ -58,7 +58,7 @@ class GitHub::Data::SQLTest < Minitest::Test
def test_initialize_with_binds def test_initialize_with_binds
binds = { :key => "value" } binds = { :key => "value" }
sql = GitHub::Data::SQL.new binds sql = GitHub::KV::SQL.new binds
assert_equal "", sql.query assert_equal "", sql.query
assert_equal "value", sql.binds[:key] assert_equal "value", sql.binds[:key]
@ -66,49 +66,49 @@ class GitHub::Data::SQLTest < Minitest::Test
end end
def test_initialize_with_query_and_binds 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 "query 'value'", sql.query
assert_equal "value", sql.binds[:key] assert_equal "value", sql.binds[:key]
end end
def test_initialize_with_single_character_binds 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 "query 'y'", sql.query
assert_equal "y", sql.binds[:x] assert_equal "y", sql.binds[:x]
end end
def test_add def test_add
sql = GitHub::Data::SQL.new sql = GitHub::KV::SQL.new
sql.add("first").add "second" sql.add("first").add "second"
assert_equal "first second", sql.query assert_equal "first second", sql.query
end end
def test_add_with_binds def test_add_with_binds
sql = GitHub::Data::SQL.new sql = GitHub::KV::SQL.new
sql.add ":local", :local => "value" sql.add ":local", :local => "value"
assert_equal "'value'", sql.query 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 sql.add ":local" # the previous value doesn't persist
end end
end end
def test_add_with_leading_and_trailing_whitespace 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 assert_equal "query", sql.query
end end
def test_add_date def test_add_date
now = Time.now.utc 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 assert_equal "'#{now.to_s(:db)}'", sql.query
end end
def test_bind def test_bind
sql = GitHub::Data::SQL.new sql = GitHub::KV::SQL.new
sql.bind(:first => "firstval").bind(:second => "secondval") sql.bind(:first => "firstval").bind(:second => "secondval")
assert_equal "firstval", sql.binds[:first] assert_equal "firstval", sql.binds[:first]
@ -116,7 +116,7 @@ class GitHub::Data::SQLTest < Minitest::Test
end end
def test_initialize_with_connection 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_equal "stub", sql.connection
assert_nil sql.binds[:connection] assert_nil sql.binds[:connection]
@ -126,65 +126,65 @@ class GitHub::Data::SQLTest < Minitest::Test
first, second = nil first, second = nil
ActiveRecord::Base.cache do ActiveRecord::Base.cache do
first = GitHub::Data::SQL.new("SELECT RAND()").value first = GitHub::KV::SQL.new("SELECT RAND()").value
second = GitHub::Data::SQL.new("SELECT RAND()").value second = GitHub::KV::SQL.new("SELECT RAND()").value
end end
assert_in_delta first, second assert_in_delta first, second
end end
def test_add_unless_empty_adds_to_a_non_empty_query 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" sql.add_unless_empty "foo"
assert_includes sql.query, "foo" assert_includes sql.query, "foo"
end end
def test_add_unless_empty_does_not_add_to_an_empty_query 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" sql.add_unless_empty "foo"
refute_includes sql.query, "foo" refute_includes sql.query, "foo"
end end
def test_literal 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 end
def test_rows 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 end
def test_rows_raises_if_non_arrays_are_provided def test_rows_raises_if_non_arrays_are_provided
assert_raises(ArgumentError) do assert_raises(ArgumentError) do
GitHub::Data::SQL::ROWS([1, 2, 3]) GitHub::KV::SQL::ROWS([1, 2, 3])
end end
end end
def test_affected_rows def test_affected_rows
begin begin
GitHub::Data::SQL.run("CREATE TEMPORARY TABLE affected_rows_test (x INT)") GitHub::KV::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("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 sql.run
assert_equal 4, sql.affected_rows assert_equal 4, sql.affected_rows
ensure ensure
GitHub::Data::SQL.run("DROP TABLE affected_rows_test") GitHub::KV::SQL.run("DROP TABLE affected_rows_test")
end end
end end
def test_affected_rows_even_when_query_generates_warning def test_affected_rows_even_when_query_generates_warning
begin begin
GitHub::Data::SQL.run("CREATE TEMPORARY TABLE affected_rows_test (x INT)") GitHub::KV::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("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'") sql = GitHub::KV::SQL.new("UPDATE affected_rows_test SET x = x + 1 WHERE 1 = '1x'")
sql.run sql.run
assert_equal 4, sql.affected_rows assert_equal 4, sql.affected_rows
ensure ensure
GitHub::Data::SQL.run("DROP TABLE affected_rows_test") GitHub::KV::SQL.run("DROP TABLE affected_rows_test")
end end
end end
end end

Просмотреть файл

@ -1,14 +1,14 @@
require "test_helper" require "test_helper"
class GitHub::Data::KVTest < Minitest::Test class GitHub::KVTest < Minitest::Test
def setup def setup
ActiveRecord::Base.connection.execute("TRUNCATE `key_values`") ActiveRecord::Base.connection.execute("TRUNCATE `key_values`")
@kv = GitHub::Data::KV.new { ActiveRecord::Base.connection } @kv = GitHub::KV.new { ActiveRecord::Base.connection }
end end
def test_initialize_without_connection def test_initialize_without_connection
kv = GitHub::Data::KV.new kv = GitHub::KV.new
assert_raises GitHub::Data::KV::MissingConnectionError do assert_raises GitHub::KV::MissingConnectionError do
kv.get("foo").value! kv.get("foo").value!
end end
end end
@ -41,7 +41,7 @@ class GitHub::Data::KVTest < Minitest::Test
def test_set_failure def test_set_failure
ActiveRecord::Base.connection.stubs(:insert).raises(Errno::ECONNRESET) 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") @kv.set("foo", "bar")
end end
end end
@ -71,7 +71,7 @@ class GitHub::Data::KVTest < Minitest::Test
def test_setnx_failure def test_setnx_failure
ActiveRecord::Base.connection.stubs(:delete).raises(Errno::ECONNRESET) 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") @kv.setnx("foo", "bar")
end end
end end
@ -86,7 +86,7 @@ class GitHub::Data::KVTest < Minitest::Test
def test_del_failure def test_del_failure
ActiveRecord::Base.connection.stubs(:delete).raises(Errno::ECONNRESET) ActiveRecord::Base.connection.stubs(:delete).raises(Errno::ECONNRESET)
assert_raises GitHub::Data::KV::UnavailableError do assert_raises GitHub::KV::UnavailableError do
@kv.del("foo") @kv.del("foo")
end end
end end
@ -106,7 +106,7 @@ class GitHub::Data::KVTest < Minitest::Test
@kv.set("foo", "bar", expires: expires) @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' SELECT expires_at FROM key_values WHERE `key` = 'foo'
SQL SQL
end end
@ -116,7 +116,7 @@ class GitHub::Data::KVTest < Minitest::Test
@kv.setnx("foo", "bar", expires: expires) @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' SELECT expires_at FROM key_values WHERE `key` = 'foo'
SQL SQL
end end
@ -145,7 +145,7 @@ class GitHub::Data::KVTest < Minitest::Test
@kv.set("foo", "bar", expires: 1.hour.from_now) @kv.set("foo", "bar", expires: 1.hour.from_now)
@kv.set("foo", "bar") @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" SELECT expires_at FROM key_values WHERE `key` = "foo"
SQL SQL
end end
@ -165,7 +165,7 @@ class GitHub::Data::KVTest < Minitest::Test
end end
def test_length_checks_key def test_length_checks_key
assert_raises GitHub::Data::KV::KeyLengthError do assert_raises GitHub::KV::KeyLengthError do
@kv.get("A" * 256) @kv.get("A" * 256)
end end
end end
@ -177,7 +177,7 @@ class GitHub::Data::KVTest < Minitest::Test
end end
def test_length_checks_value def test_length_checks_value
assert_raises GitHub::Data::KV::ValueLengthError do assert_raises GitHub::KV::ValueLengthError do
@kv.set("foo", "A" * 65536) @kv.set("foo", "A" * 65536)
end end
end end

Просмотреть файл

@ -1,6 +1,6 @@
require "bundler/setup" require "bundler/setup"
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require "github/data" require "github/kv"
require "timecop" require "timecop"
require "minitest/autorun" require "minitest/autorun"