зеркало из https://github.com/github/github-ds.git
Rename from generic github-data to specific github-kv
This commit is contained in:
Родитель
5904050f4a
Коммит
c3fb2faeb3
|
@ -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
|
||||||
|
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -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'}"
|
||||||
|
|
37
README.md
37
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::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
|
|
|
@ -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"
|
||||||
|
|
Загрузка…
Ссылка в новой задаче