github-ds 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/Rakefile +10 -0
- data/examples/example_setup.rb +38 -0
- data/examples/kv.rb +45 -0
- data/examples/result.rb +50 -0
- data/examples/sql.rb +44 -0
- data/examples/sql_add.rb +42 -0
- data/examples/sql_with_connection.rb +32 -0
- data/github-ds.gemspec +42 -0
- data/lib/generators/github/ds/active_record_generator.rb +22 -0
- data/lib/generators/github/ds/templates/migration.rb +19 -0
- data/lib/github-ds.rb +1 -0
- data/lib/github/ds.rb +8 -0
- data/lib/github/ds/version.rb +5 -0
- data/lib/github/kv.rb +343 -0
- data/lib/github/result.rb +229 -0
- data/lib/github/sql.rb +447 -0
- data/script/bootstrap +6 -0
- data/script/console +14 -0
- data/script/install +6 -0
- data/script/release +6 -0
- data/script/test +6 -0
- metadata +202 -0
data/examples/result.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path("../example_setup", __FILE__)
|
2
|
+
require "github/result"
|
3
|
+
|
4
|
+
def do_something
|
5
|
+
1
|
6
|
+
end
|
7
|
+
|
8
|
+
def do_something_that_errors
|
9
|
+
raise "noooooppppeeeee"
|
10
|
+
end
|
11
|
+
|
12
|
+
result = GitHub::Result.new { do_something }
|
13
|
+
p result.ok? # => true
|
14
|
+
p result.value! # => 1
|
15
|
+
|
16
|
+
result = GitHub::Result.new { do_something_that_errors }
|
17
|
+
p result.ok? # => false
|
18
|
+
p result.value { "default when error happens" } # => "default when error happens"
|
19
|
+
|
20
|
+
begin
|
21
|
+
result.value! # => raises exception because error happened
|
22
|
+
rescue => error
|
23
|
+
p result.error # => the error
|
24
|
+
p error # the same error
|
25
|
+
end
|
26
|
+
|
27
|
+
# Outputs Step 1, 2, 3
|
28
|
+
result = GitHub::Result.new {
|
29
|
+
GitHub::Result.new { puts "Step 1: success!" }
|
30
|
+
}.then { |value|
|
31
|
+
GitHub::Result.new { puts "Step 2: success!" }
|
32
|
+
}.then { |value|
|
33
|
+
GitHub::Result.new { puts "Step 3: success!" }
|
34
|
+
}
|
35
|
+
p result.ok? # => true
|
36
|
+
|
37
|
+
# Outputs Step 1, 2 and stops.
|
38
|
+
result = GitHub::Result.new {
|
39
|
+
GitHub::Result.new { puts "Step 1: success!" }
|
40
|
+
}.then { |value|
|
41
|
+
GitHub::Result.new {
|
42
|
+
puts "Step 2: failed!"
|
43
|
+
raise
|
44
|
+
}
|
45
|
+
}.then { |value|
|
46
|
+
GitHub::Result.new {
|
47
|
+
puts "Step 3: should not get here because previous step failed!"
|
48
|
+
}
|
49
|
+
}
|
50
|
+
p result.ok? # => false
|
data/examples/sql.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path("../example_setup", __FILE__)
|
2
|
+
require "github/sql"
|
3
|
+
|
4
|
+
insert_statement = "INSERT INTO example_key_values (`key`, `value`) VALUES (:key, :value)"
|
5
|
+
select_statement = "SELECT value FROM example_key_values WHERE `key` = :key"
|
6
|
+
update_statement = "UPDATE example_key_values SET value = :value WHERE `key` = :key"
|
7
|
+
delete_statement = "DELETE FROM example_key_values WHERE `key` = :key"
|
8
|
+
|
9
|
+
################################# Class Style ##################################
|
10
|
+
sql = GitHub::SQL.run insert_statement, key: "foo", value: "bar"
|
11
|
+
p sql.last_insert_id
|
12
|
+
# 1
|
13
|
+
|
14
|
+
p GitHub::SQL.value select_statement, key: "foo"
|
15
|
+
# "bar"
|
16
|
+
|
17
|
+
sql = GitHub::SQL.run update_statement, key: "foo", value: "new value"
|
18
|
+
p sql.affected_rows
|
19
|
+
# 1
|
20
|
+
|
21
|
+
sql = GitHub::SQL.run delete_statement, key: "foo"
|
22
|
+
p sql.affected_rows
|
23
|
+
# 1
|
24
|
+
|
25
|
+
|
26
|
+
################################ Instance Style ################################
|
27
|
+
sql = GitHub::SQL.new insert_statement, key: "foo", value: "bar"
|
28
|
+
sql.run
|
29
|
+
p sql.last_insert_id
|
30
|
+
# 2
|
31
|
+
|
32
|
+
sql = GitHub::SQL.new select_statement, key: "foo"
|
33
|
+
p sql.value
|
34
|
+
# "bar"
|
35
|
+
|
36
|
+
sql = GitHub::SQL.new update_statement, key: "foo", value: "new value"
|
37
|
+
sql.run
|
38
|
+
p sql.affected_rows
|
39
|
+
# 1
|
40
|
+
|
41
|
+
sql = GitHub::SQL.new delete_statement, key: "foo"
|
42
|
+
sql.run
|
43
|
+
p sql.affected_rows
|
44
|
+
# 1
|
data/examples/sql_add.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path("../example_setup", __FILE__)
|
2
|
+
require "github/sql"
|
3
|
+
|
4
|
+
GitHub::SQL.run <<-SQL
|
5
|
+
INSERT INTO example_key_values (`key`, `value`)
|
6
|
+
VALUES ("foo", "bar"), ("baz", "wick")
|
7
|
+
SQL
|
8
|
+
|
9
|
+
sql = GitHub::SQL.new "SELECT `VALUE` FROM example_key_values"
|
10
|
+
|
11
|
+
key = ENV["KEY"]
|
12
|
+
unless key.nil?
|
13
|
+
sql.add "WHERE `key` = :key", key: key
|
14
|
+
end
|
15
|
+
|
16
|
+
limit = ENV["LIMIT"]
|
17
|
+
unless limit.nil?
|
18
|
+
sql.add "ORDER BY `key` ASC"
|
19
|
+
sql.add "LIMIT :limit", limit: limit.to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
p sql.results
|
23
|
+
|
24
|
+
# Only select value for key = foo
|
25
|
+
# $ env KEY=foo bundle exec ruby examples/sql_add.rb
|
26
|
+
# [["bar"]]
|
27
|
+
#
|
28
|
+
# Only select value for key = bar
|
29
|
+
# $ env KEY=bar bundle exec ruby examples/sql_add.rb
|
30
|
+
# []
|
31
|
+
#
|
32
|
+
# Only select value for key = baz
|
33
|
+
# $ env KEY=baz bundle exec ruby examples/sql_add.rb
|
34
|
+
# [["wick"]]
|
35
|
+
#
|
36
|
+
# Select all values
|
37
|
+
# $ bundle exec ruby examples/sql_add.rb
|
38
|
+
# [["bar"], ["wick"]]
|
39
|
+
#
|
40
|
+
# Select only 1 key.
|
41
|
+
# $ env LIMIT=1 bundle exec ruby examples/sql_add.rb
|
42
|
+
# [["wick"]]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path("../example_setup", __FILE__)
|
2
|
+
require "github/sql"
|
3
|
+
|
4
|
+
class SomeModel < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
establish_connection({
|
8
|
+
adapter: "mysql2",
|
9
|
+
database: "github_ds_test",
|
10
|
+
})
|
11
|
+
end
|
12
|
+
|
13
|
+
insert_statement = "INSERT INTO example_key_values (`key`, `value`) VALUES (:key, :value)"
|
14
|
+
|
15
|
+
ActiveRecord::Base.transaction do
|
16
|
+
# Insert bar on base connection.
|
17
|
+
GitHub::SQL.run insert_statement, key: "bar", value: "baz", connection: ActiveRecord::Base.connection
|
18
|
+
|
19
|
+
SomeModel.transaction do
|
20
|
+
# Insert foo on different connection.
|
21
|
+
GitHub::SQL.run insert_statement, key: "foo", value: "bar", connection: SomeModel.connection
|
22
|
+
end
|
23
|
+
|
24
|
+
# Roll back "bar" insertion.
|
25
|
+
raise ActiveRecord::Rollback
|
26
|
+
end
|
27
|
+
|
28
|
+
# Show that "bar" key is not here because that connection's transaction was
|
29
|
+
# rolled back. SomeModel is a different connection and started a different
|
30
|
+
# transaction, which succeeded, so "foo" key was created.
|
31
|
+
p GitHub::SQL.values "SELECT `key` FROM example_key_values"
|
32
|
+
# ["foo"]
|
data/github-ds.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "github/ds/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "github-ds"
|
8
|
+
spec.version = GitHub::DS::VERSION
|
9
|
+
spec.authors = ["GitHub Open Source", "John Nunemaker"]
|
10
|
+
spec.email = ["opensource+github-ds@github.com", "nunemaker@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A collection of libraries for working with SQL on top of ActiveRecord's connection.}
|
13
|
+
spec.description = %q{A collection of libraries for working with SQL on top of ActiveRecord's connection.}
|
14
|
+
spec.homepage = "https://github.com/github/github-ds"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
34
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
spec.add_development_dependency "timecop", "~> 0.8.1"
|
37
|
+
spec.add_development_dependency "activerecord", "~> 5.0"
|
38
|
+
spec.add_development_dependency "activesupport"
|
39
|
+
spec.add_development_dependency "mysql2"
|
40
|
+
spec.add_development_dependency "activerecord-mysql-adapter"
|
41
|
+
spec.add_development_dependency "mocha", "~> 1.2.1"
|
42
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "rails/generators/active_record"
|
2
|
+
|
3
|
+
module Github
|
4
|
+
module DS
|
5
|
+
module Generators
|
6
|
+
class ActiveRecordGenerator < ::Rails::Generators::Base
|
7
|
+
include ::Rails::Generators::Migration
|
8
|
+
desc "Generates migration for KV table"
|
9
|
+
|
10
|
+
source_paths << File.join(File.dirname(__FILE__), "templates")
|
11
|
+
|
12
|
+
def create_migration_file
|
13
|
+
migration_template "migration.rb", "db/migrate/create_key_values_table.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.next_migration_number(dirname)
|
17
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateKeyValuesTable < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :key_values do |t|
|
4
|
+
t.string :key, :null => false
|
5
|
+
t.binary :value, :null => false
|
6
|
+
t.datetime :expires_at, :null => true
|
7
|
+
t.timestamps :null => false
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :key_values, :key, :unique => true
|
11
|
+
add_index :key_values, :expires_at
|
12
|
+
|
13
|
+
change_column :key_values, :id, "bigint(20) NOT NULL AUTO_INCREMENT"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.down
|
17
|
+
drop_table :key_values
|
18
|
+
end
|
19
|
+
end
|
data/lib/github-ds.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "github/ds"
|
data/lib/github/ds.rb
ADDED
data/lib/github/kv.rb
ADDED
@@ -0,0 +1,343 @@
|
|
1
|
+
require "github/result"
|
2
|
+
require "github/sql"
|
3
|
+
|
4
|
+
# GitHub::KV is a key/value data store backed by MySQL (however, the backing
|
5
|
+
# store used should be regarded as an implementation detail).
|
6
|
+
#
|
7
|
+
# Usage tips:
|
8
|
+
#
|
9
|
+
# * Components in key names should be ordered by cardinality, from lowest to
|
10
|
+
# highest. That is, static key components should be at the front of the key
|
11
|
+
# and key components that vary should be at the end of the key in order of
|
12
|
+
# how many potential values they might have.
|
13
|
+
#
|
14
|
+
# For example, if using GitHub::KV to store a user preferences, the key
|
15
|
+
# should be named "user.#{preference_name}.#{user_id}". Notice that the
|
16
|
+
# part of the key that never changes ("user") comes first, followed by
|
17
|
+
# the name of the preference (of which there might be a handful), followed
|
18
|
+
# finally by the user id (of which there are millions).
|
19
|
+
#
|
20
|
+
# This will make it easier to scan for keys later on, which is a necessity
|
21
|
+
# if we ever need to move this data out of GitHub::KV or if we need to
|
22
|
+
# search the keyspace for some reason (for example, if it's a preference
|
23
|
+
# that we're planning to deprecate, putting the preference name near the
|
24
|
+
# beginning of the key name makes it easier to search for all users with
|
25
|
+
# that preference set).
|
26
|
+
#
|
27
|
+
# * All reader methods in GitHub::KV return values wrapped inside a Result
|
28
|
+
# object.
|
29
|
+
#
|
30
|
+
# If any of these methods raise an exception for some reason (for example,
|
31
|
+
# the database is down), they will return a Result value representing this
|
32
|
+
# error rather than raising the exception directly. See lib/github/result.rb
|
33
|
+
# for more documentation on GitHub::Result including usage examples.
|
34
|
+
#
|
35
|
+
# When using GitHub::KV, it's important to handle error conditions and not
|
36
|
+
# assume that GitHub::Result objects will always represent success.
|
37
|
+
# Code using GitHub::KV should be able to fail partially if
|
38
|
+
# GitHub::KV is down. How exactly to do this will depend on a
|
39
|
+
# case-by-case basis - it may involve falling back to a default value, or it
|
40
|
+
# might involve showing an error message to the user while still letting the
|
41
|
+
# rest of the page load.
|
42
|
+
#
|
43
|
+
module GitHub
|
44
|
+
class KV
|
45
|
+
MAX_KEY_LENGTH = 255
|
46
|
+
MAX_VALUE_LENGTH = 65535
|
47
|
+
|
48
|
+
KeyLengthError = Class.new(StandardError)
|
49
|
+
ValueLengthError = Class.new(StandardError)
|
50
|
+
UnavailableError = Class.new(StandardError)
|
51
|
+
|
52
|
+
class MissingConnectionError < StandardError; end
|
53
|
+
|
54
|
+
def initialize(encapsulated_errors = [SystemCallError], &conn_block)
|
55
|
+
@encapsulated_errors = encapsulated_errors
|
56
|
+
@conn_block = conn_block
|
57
|
+
end
|
58
|
+
|
59
|
+
def connection
|
60
|
+
@conn_block.try(:call) || (raise MissingConnectionError, "KV must be initialized with a block that returns a connection")
|
61
|
+
end
|
62
|
+
|
63
|
+
# get :: String -> Result<String | nil>
|
64
|
+
#
|
65
|
+
# Gets the value of the specified key.
|
66
|
+
#
|
67
|
+
# Example:
|
68
|
+
#
|
69
|
+
# kv.get("foo")
|
70
|
+
# # => #<Result value: "bar">
|
71
|
+
#
|
72
|
+
# kv.get("octocat")
|
73
|
+
# # => #<Result value: nil>
|
74
|
+
#
|
75
|
+
def get(key)
|
76
|
+
validate_key(key)
|
77
|
+
|
78
|
+
mget([key]).map { |values| values[0] }
|
79
|
+
end
|
80
|
+
|
81
|
+
# mget :: [String] -> Result<[String | nil]>
|
82
|
+
#
|
83
|
+
# Gets the values of all specified keys. Values will be returned in the
|
84
|
+
# same order as keys are specified. nil will be returned in place of a
|
85
|
+
# String for keys which do not exist.
|
86
|
+
#
|
87
|
+
# Example:
|
88
|
+
#
|
89
|
+
# kv.mget(["foo", "octocat"])
|
90
|
+
# # => #<Result value: ["bar", nil]
|
91
|
+
#
|
92
|
+
def mget(keys)
|
93
|
+
validate_key_array(keys)
|
94
|
+
|
95
|
+
Result.new {
|
96
|
+
kvs = GitHub::SQL.results(<<-SQL, :keys => keys, :connection => connection).to_h
|
97
|
+
SELECT `key`, value FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > NOW())
|
98
|
+
SQL
|
99
|
+
|
100
|
+
keys.map { |key| kvs[key] }
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# set :: String, String, expires: Time? -> nil
|
105
|
+
#
|
106
|
+
# Sets the specified key to the specified value. Returns nil. Raises on
|
107
|
+
# error.
|
108
|
+
#
|
109
|
+
# Example:
|
110
|
+
#
|
111
|
+
# kv.set("foo", "bar")
|
112
|
+
# # => nil
|
113
|
+
#
|
114
|
+
def set(key, value, expires: nil)
|
115
|
+
validate_key(key)
|
116
|
+
validate_value(value)
|
117
|
+
|
118
|
+
mset({ key => value }, expires: expires)
|
119
|
+
end
|
120
|
+
|
121
|
+
# mset :: { String => String }, expires: Time? -> nil
|
122
|
+
#
|
123
|
+
# Sets the specified hash keys to their associated values, setting them to
|
124
|
+
# expire at the specified time. Returns nil. Raises on error.
|
125
|
+
#
|
126
|
+
# Example:
|
127
|
+
#
|
128
|
+
# kv.mset({ "foo" => "bar", "baz" => "quux" })
|
129
|
+
# # => nil
|
130
|
+
#
|
131
|
+
# kv.mset({ "expires" => "soon" }, expires: 1.hour.from_now)
|
132
|
+
# # => nil
|
133
|
+
#
|
134
|
+
def mset(kvs, expires: nil)
|
135
|
+
validate_key_value_hash(kvs)
|
136
|
+
validate_expires(expires) if expires
|
137
|
+
|
138
|
+
rows = kvs.map { |key, value|
|
139
|
+
[key, value, GitHub::SQL::NOW, GitHub::SQL::NOW, expires || GitHub::SQL::NULL]
|
140
|
+
}
|
141
|
+
|
142
|
+
encapsulate_error do
|
143
|
+
GitHub::SQL.run(<<-SQL, :rows => GitHub::SQL::ROWS(rows), :connection => connection)
|
144
|
+
INSERT INTO key_values (`key`, value, created_at, updated_at, expires_at)
|
145
|
+
VALUES :rows
|
146
|
+
ON DUPLICATE KEY UPDATE
|
147
|
+
value = VALUES(value),
|
148
|
+
updated_at = VALUES(updated_at),
|
149
|
+
expires_at = VALUES(expires_at)
|
150
|
+
SQL
|
151
|
+
end
|
152
|
+
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
# exists :: String -> Result<Boolean>
|
157
|
+
#
|
158
|
+
# Checks for existence of the specified key.
|
159
|
+
#
|
160
|
+
# Example:
|
161
|
+
#
|
162
|
+
# kv.exists("foo")
|
163
|
+
# # => #<Result value: true>
|
164
|
+
#
|
165
|
+
# kv.exists("octocat")
|
166
|
+
# # => #<Result value: false>
|
167
|
+
#
|
168
|
+
def exists(key)
|
169
|
+
validate_key(key)
|
170
|
+
|
171
|
+
mexists([key]).map { |values| values[0] }
|
172
|
+
end
|
173
|
+
|
174
|
+
# mexists :: [String] -> Result<[Boolean]>
|
175
|
+
#
|
176
|
+
# Checks for existence of all specified keys. Booleans will be returned in
|
177
|
+
# the same order as keys are specified.
|
178
|
+
#
|
179
|
+
# Example:
|
180
|
+
#
|
181
|
+
# kv.mexists(["foo", "octocat"])
|
182
|
+
# # => #<Result value: [true, false]>
|
183
|
+
#
|
184
|
+
def mexists(keys)
|
185
|
+
validate_key_array(keys)
|
186
|
+
|
187
|
+
Result.new {
|
188
|
+
existing_keys = GitHub::SQL.values(<<-SQL, :keys => keys, :connection => connection).to_set
|
189
|
+
SELECT `key` FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > NOW())
|
190
|
+
SQL
|
191
|
+
|
192
|
+
keys.map { |key| existing_keys.include?(key) }
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
# setnx :: String, String, expires: Time? -> Boolean
|
197
|
+
#
|
198
|
+
# Sets the specified key to the specified value only if it does not
|
199
|
+
# already exist.
|
200
|
+
#
|
201
|
+
# Returns true if the key was set, false otherwise. Raises on error.
|
202
|
+
#
|
203
|
+
# Example:
|
204
|
+
#
|
205
|
+
# kv.setnx("foo", "bar")
|
206
|
+
# # => false
|
207
|
+
#
|
208
|
+
# kv.setnx("octocat", "monalisa")
|
209
|
+
# # => true
|
210
|
+
#
|
211
|
+
# kv.setnx("expires", "soon", expires: 1.hour.from_now)
|
212
|
+
# # => true
|
213
|
+
#
|
214
|
+
def setnx(key, value, expires: nil)
|
215
|
+
validate_key(key)
|
216
|
+
validate_value(value)
|
217
|
+
validate_expires(expires) if expires
|
218
|
+
|
219
|
+
encapsulate_error {
|
220
|
+
# if the key already exists but has expired, prune it first. We could
|
221
|
+
# achieve the same thing with the right INSERT ... ON DUPLICATE KEY UPDATE
|
222
|
+
# query, but then we would not be able to rely on affected_rows
|
223
|
+
|
224
|
+
GitHub::SQL.run(<<-SQL, :key => key, :connection => connection)
|
225
|
+
DELETE FROM key_values WHERE `key` = :key AND expires_at <= NOW()
|
226
|
+
SQL
|
227
|
+
|
228
|
+
sql = GitHub::SQL.run(<<-SQL, :key => key, :value => value, :expires => expires || GitHub::SQL::NULL, :connection => connection)
|
229
|
+
INSERT IGNORE INTO key_values (`key`, value, created_at, updated_at, expires_at)
|
230
|
+
VALUES (:key, :value, NOW(), NOW(), :expires)
|
231
|
+
SQL
|
232
|
+
|
233
|
+
sql.affected_rows > 0
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
# del :: String -> nil
|
238
|
+
#
|
239
|
+
# Deletes the specified key. Returns nil. Raises on error.
|
240
|
+
#
|
241
|
+
# Example:
|
242
|
+
#
|
243
|
+
# kv.del("foo")
|
244
|
+
# # => nil
|
245
|
+
#
|
246
|
+
def del(key)
|
247
|
+
validate_key(key)
|
248
|
+
|
249
|
+
mdel([key])
|
250
|
+
end
|
251
|
+
|
252
|
+
# mdel :: String -> nil
|
253
|
+
#
|
254
|
+
# Deletes the specified keys. Returns nil. Raises on error.
|
255
|
+
#
|
256
|
+
# Example:
|
257
|
+
#
|
258
|
+
# kv.mdel(["foo", "octocat"])
|
259
|
+
# # => nil
|
260
|
+
#
|
261
|
+
def mdel(keys)
|
262
|
+
validate_key_array(keys)
|
263
|
+
|
264
|
+
encapsulate_error do
|
265
|
+
GitHub::SQL.run(<<-SQL, :keys => keys, :connection => connection)
|
266
|
+
DELETE FROM key_values WHERE `key` IN :keys
|
267
|
+
SQL
|
268
|
+
end
|
269
|
+
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
def validate_key(key)
|
275
|
+
raise TypeError, "key must be a String in #{self.class.name}, but was #{key.class}" unless key.is_a?(String)
|
276
|
+
|
277
|
+
validate_key_length(key)
|
278
|
+
end
|
279
|
+
|
280
|
+
def validate_value(value)
|
281
|
+
raise TypeError, "value must be a String in #{self.class.name}, but was #{value.class}" unless value.is_a?(String)
|
282
|
+
|
283
|
+
validate_value_length(value)
|
284
|
+
end
|
285
|
+
|
286
|
+
def validate_key_array(keys)
|
287
|
+
unless keys.is_a?(Array)
|
288
|
+
raise TypeError, "keys must be a [String] in #{self.class.name}, but was #{keys.class}"
|
289
|
+
end
|
290
|
+
|
291
|
+
keys.each do |key|
|
292
|
+
unless key.is_a?(String)
|
293
|
+
raise TypeError, "keys must be a [String] in #{self.class.name}, but also saw at least one #{key.class}"
|
294
|
+
end
|
295
|
+
|
296
|
+
validate_key_length(key)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def validate_key_value_hash(kvs)
|
301
|
+
unless kvs.is_a?(Hash)
|
302
|
+
raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but was #{key.class}"
|
303
|
+
end
|
304
|
+
|
305
|
+
kvs.each do |key, value|
|
306
|
+
unless key.is_a?(String)
|
307
|
+
raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but also saw at least one key of type #{key.class}"
|
308
|
+
end
|
309
|
+
|
310
|
+
unless value.is_a?(String)
|
311
|
+
raise TypeError, "kvs must be a {String => String} in #{self.class.name}, but also saw at least one value of type #{value.class}"
|
312
|
+
end
|
313
|
+
|
314
|
+
validate_key_length(key)
|
315
|
+
validate_value_length(value)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def validate_key_length(key)
|
320
|
+
if key.length > MAX_KEY_LENGTH
|
321
|
+
raise KeyLengthError, "key of length #{key.length} exceeds maximum key length of #{MAX_KEY_LENGTH}\n\nkey: #{key.inspect}"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def validate_value_length(value)
|
326
|
+
if value.length > MAX_VALUE_LENGTH
|
327
|
+
raise ValueLengthError, "value of length #{value.length} exceeds maximum value length of #{MAX_VALUE_LENGTH}"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def validate_expires(expires)
|
332
|
+
unless expires.respond_to?(:to_time)
|
333
|
+
raise TypeError, "expires must be a time of some sort (Time, DateTime, ActiveSupport::TimeWithZone, etc.), but was #{expires.class}"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def encapsulate_error
|
338
|
+
yield
|
339
|
+
rescue *@encapsulated_errors => error
|
340
|
+
raise UnavailableError, "#{error.class}: #{error.message}"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|