├── lib ├── leveldb │ ├── version.rb │ ├── snapshot.rb │ ├── batch.rb │ ├── iterator.rb │ └── db.rb ├── leveldb.rb └── native.rb ├── .gitmodules ├── test ├── helper.rb ├── test_snapshot.rb ├── test_batch.rb ├── test_db.rb └── test_iterator.rb ├── .gitignore ├── Gemfile ├── ext └── Rakefile ├── benchmark ├── leak.rb ├── leveldb-ruby.rb └── leveldb.rb ├── LICENSE.txt ├── leveldb.gemspec ├── Rakefile └── README.md /lib/leveldb/version.rb: -------------------------------------------------------------------------------- 1 | module LevelDB 2 | VERSION = '0.1.9' 3 | end 4 | -------------------------------------------------------------------------------- /lib/leveldb.rb: -------------------------------------------------------------------------------- 1 | require 'native' 2 | require 'leveldb/db' 3 | 4 | module LevelDB 5 | C = Native 6 | end 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/leveldb"] 2 | path = ext/leveldb 3 | url = https://github.com/google/leveldb.git 4 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'leveldb' 3 | require 'minitest/autorun' 4 | 5 | # Create a temp directory 6 | Dir.mkdir './tmp' unless Dir.exist?('./tmp') 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'yard' 5 | gem 'redcarpet' 6 | 7 | if dev_ffi = File.expand_path('../../ffi-gen', __FILE__) and File.exist?(dev_ffi) 8 | gem 'ffi-gen', require: 'ffi/gen', path: dev_ffi 9 | else 10 | gem 'ffi-gen', require: 'ffi/gen', github: 'DAddYE/ffi-gen' 11 | end 12 | -------------------------------------------------------------------------------- /ext/Rakefile: -------------------------------------------------------------------------------- 1 | task :default do 2 | leveldb_dir = File.expand_path('../leveldb', __FILE__) 3 | 4 | unless File.directory?(leveldb_dir) 5 | STDERR.puts "ext/leveldb missing, please checkout its submodule..." 6 | exit 1 7 | end 8 | 9 | # Make sure leveldb is built... 10 | system "cd #{leveldb_dir} && make" || exit(1) 11 | end 12 | -------------------------------------------------------------------------------- /benchmark/leak.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'leveldb' 3 | require 'pp' 4 | 5 | GC::Profiler.enable 6 | 7 | db = LevelDB::DB.new("/tmp/leaktest") 8 | p db.get 'fox' 9 | 10 | 10_000_000.times { db.get 'fox' } 11 | 12 | counts = Hash.new(0) 13 | ObjectSpace.each_object do |o| 14 | counts[o.class] += 1 15 | end 16 | 17 | pp counts.sort_by { |k, v| v } 18 | 19 | puts GC::Profiler.result 20 | # puts GC::Profiler.raw_data 21 | 22 | 23 | sleep 24 | -------------------------------------------------------------------------------- /lib/leveldb/snapshot.rb: -------------------------------------------------------------------------------- 1 | module LevelDB 2 | 3 | class Snapshot 4 | 5 | def initialize(db, read_opts) 6 | @_db = db 7 | @_read_opts = read_opts 8 | @_snap = C.create_snapshot(@_db) 9 | end 10 | 11 | def set! 12 | C.readoptions_set_snapshot(@_read_opts, @_snap) 13 | end 14 | 15 | def reset! 16 | C.readoptions_set_snapshot(@_read_opts, nil) 17 | end 18 | 19 | def inspect 20 | "#" 21 | end 22 | alias to_s inspect 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_snapshot.rb: -------------------------------------------------------------------------------- 1 | require_relative './helper' 2 | 3 | class TestSnapshot < Minitest::Test 4 | attr_reader :db 5 | 6 | def setup 7 | @db ||= LevelDB::DB.new './tmp/test-snapshot' 8 | end 9 | 10 | def teardown 11 | db.close 12 | db.destroy 13 | end 14 | 15 | def test_snap 16 | db.put 'a', 1 17 | db.put 'b', 2 18 | db.put 'c', 3 19 | 20 | snap = db.snapshot 21 | 22 | db.delete 'a' 23 | refute db.get 'a' 24 | 25 | snap.set! 26 | 27 | assert_equal '1', db.get('a') 28 | 29 | snap.reset! 30 | 31 | refute db.get('a') 32 | 33 | snap.set! 34 | 35 | assert_equal '1', db.get('a') 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/test_batch.rb: -------------------------------------------------------------------------------- 1 | require_relative './helper' 2 | 3 | class TestBatch < Minitest::Test 4 | attr_reader :db 5 | 6 | def setup 7 | @db ||= LevelDB::DB.new './tmp/test-batch' 8 | end 9 | 10 | def teardown 11 | db.close 12 | db.destroy 13 | end 14 | 15 | def test_batch 16 | batch = db.batch 17 | ('a'..'z').each do |l| 18 | refute db[l] 19 | batch.put l, l.upcase 20 | end 21 | 22 | batch.write! 23 | 24 | ('a'..'z').each do |l| 25 | assert batch.delete l 26 | assert_equal l.upcase, db[l] 27 | end 28 | 29 | batch.write! 30 | 31 | ('a'..'z').each { |l| refute db[l] } 32 | end 33 | 34 | def test_batch_block 35 | ('a'..'z').each { |l| refute db[l] } 36 | 37 | db.batch do |batch| 38 | ('a'..'z').each { |l| batch.put l, l.upcase } 39 | end 40 | 41 | ('a'..'z').each { |l| assert_equal l.upcase, db[l] } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Davide D'Agostino 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /leveldb.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'leveldb/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "leveldb" 8 | spec.version = LevelDB::VERSION 9 | spec.authors = ["DAddYE"] 10 | spec.email = ["info@daddye.it"] 11 | spec.description = "LevelDB for Ruby" 12 | spec.summary = spec.description 13 | spec.homepage = "https://github.com/DAddYE/leveldb" 14 | spec.license = "MIT" 15 | spec.files = [] 16 | 17 | if RUBY_PLATFORM =~ /java/ 18 | spec.platform = "java" 19 | else 20 | spec.extensions = Dir["ext/Rakefile"] 21 | spec.files += Dir["ext/leveldb/**/*.{c,cc,h,mk,d}"] - 22 | Dir["ext/leveldb/doc/*"] + 23 | Dir["ext/leveldb/LICENSE"] + 24 | Dir["ext/leveldb/Makefile"] + 25 | Dir["ext/leveldb/build_detect_platform"] 26 | end 27 | 28 | spec.files += Dir['lib/**/*.rb'] 29 | spec.files += %w[README.md LICENSE.txt] 30 | spec.executables = Dir['bin/**/*'] 31 | spec.test_files = Dir['test/**/*.rb'] 32 | spec.require_paths = ["lib"] 33 | 34 | spec.add_dependency "fiddler-rb", "~> 0.1.1" 35 | 36 | spec.add_development_dependency "bundler", "~> 1.3" 37 | spec.add_development_dependency "rake" 38 | spec.add_development_dependency "minitest" 39 | spec.add_development_dependency "ffi" 40 | end 41 | -------------------------------------------------------------------------------- /benchmark/leveldb-ruby.rb: -------------------------------------------------------------------------------- 1 | require 'leveldb' # be sure to a) don't add bundler/setup b) gem uninstall leveldb 2 | require 'benchmark' 3 | require 'minitest' 4 | 5 | puts '## Please wait, I\'m generating 100mb of random data ...' 6 | 7 | N = 10_240 8 | SAMPLE = [] 9 | 10 | File.open('/dev/urandom', File::RDONLY || File::NONBLOCK || File::NOCTTY) do |f| 11 | N.times { |i| SAMPLE << f.readpartial(5_120).unpack("H*")[0] } 12 | end 13 | 14 | system 'rm -rf /tmp/bench' 15 | db = LevelDB::DB.new '/tmp/bench', compression: LevelDB::CompressionType::NoCompression 16 | puts '## Without compression:' 17 | Benchmark.bm do |x| 18 | x.report('put') { N.times { |i| db.put(i.to_s, SAMPLE[i]) } } 19 | x.report('get') { N.times { |i| raise unless db.get(i.to_s) == SAMPLE[i] } } 20 | end 21 | db.close 22 | 23 | system 'rm -rf /tmp/bench' 24 | db = LevelDB::DB.new '/tmp/bench', compression: LevelDB::CompressionType::SnappyCompression 25 | puts '## With compression:' 26 | Benchmark.bm do |x| 27 | x.report('put') { N.times { |i| db.put(i.to_s, SAMPLE[i]) } } 28 | x.report('get') { N.times { |i| raise unless db.get(i.to_s) == SAMPLE[i] } } 29 | end 30 | db.close 31 | 32 | system 'rm -rf /tmp/bench' 33 | db = LevelDB::DB.new '/tmp/bench', compression: LevelDB::CompressionType::SnappyCompression 34 | puts '## With batch:' 35 | Benchmark.bm do |x| 36 | x.report 'put' do 37 | db.batch do |batch| 38 | N.times { |i| batch.put(i, SAMPLE[i]) } 39 | end 40 | end 41 | end 42 | db.close 43 | -------------------------------------------------------------------------------- /lib/leveldb/batch.rb: -------------------------------------------------------------------------------- 1 | module LevelDB 2 | 3 | class Batch 4 | class Error < StandardError; end 5 | 6 | def initialize(db, write_opts) 7 | @_db = db 8 | @_write_opts = write_opts 9 | @_err = C::Pointer.malloc(C::SIZEOF_VOIDP) 10 | @_err.free = C[:free] 11 | @_batch = C.writebatch_create 12 | end 13 | 14 | def []=(key, val) 15 | key, val = key.to_s, val.to_s 16 | C.writebatch_put(@_batch, key, key.size, val, val.size) 17 | 18 | val 19 | end 20 | alias put []= 21 | 22 | def delete(key) 23 | key = key.to_s 24 | C.writebatch_delete(@_batch, key, key.size) 25 | 26 | key 27 | end 28 | 29 | # def clear 30 | # C.writebatch_clear(@_batch) 31 | 32 | # true 33 | # end 34 | # alias clear! clear 35 | 36 | def write! 37 | C.write(@_db, @_write_opts, @_batch, @_err) 38 | 39 | raise Error, error_message if errors? 40 | 41 | true 42 | end 43 | 44 | def errors? 45 | return unless @_err 46 | !@_err.ptr.null? 47 | end 48 | 49 | def error_message 50 | return unless errors? 51 | @_err.ptr.to_s 52 | ensure 53 | if errors? 54 | @_err = C::Pointer.malloc(C::SIZEOF_VOIDP) 55 | @_err.free = C[:free] 56 | end 57 | end 58 | alias clear_errors! error_message 59 | 60 | def inspect 61 | "#" 62 | end 63 | alias to_s inspect 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'bundler/setup' 3 | require 'ffi/gen' 4 | require 'rake/testtask' 5 | require 'yard' 6 | 7 | task :default => :test 8 | 9 | task :check do 10 | sh 'git submodule update --init' unless File.exist?('ext/leveldb/.git') 11 | end 12 | 13 | desc "Generates leveldb ext" 14 | task :compile => :check do 15 | sh 'cd ext && rake' 16 | end 17 | 18 | desc "Clean leveldb build" 19 | task :clean do 20 | sh 'cd ext/leveldb && make clean' 21 | end 22 | 23 | task :release => :clean 24 | 25 | desc "Rebuild leveldb" 26 | task :rebuild => [:clean, :compile] 27 | 28 | default_config = -> (header, suffix="", extra={}) do 29 | { 30 | module_name: 'Leveldb', 31 | ffi_lib: 'File.expand_path("../../ext/leveldb/libleveldb.#{FFI::Platform::LIBSUFFIX}", __FILE__)', 32 | headers: [File.expand_path("../ext/leveldb/include/leveldb/#{header}", __FILE__)], 33 | cflags: `llvm-config --cflags`.split(" "), 34 | prefixes: ['leveldb_'], 35 | suffixes: ['_t', '_s'], 36 | output: "lib/native#{suffix}.rb" 37 | }.merge(extra) 38 | end 39 | 40 | Rake::TestTask.new do |t| 41 | t.libs << 'test' 42 | t.test_files = FileList['test/test_*.rb'] 43 | end 44 | 45 | desc 'Generates FFI bindings' 46 | task :ffi => :check do 47 | raise 'Please install llvm' unless system('which llvm-config') 48 | FFI::Gen.generate(default_config["c.h"]) 49 | end 50 | 51 | desc 'Open a console' 52 | task :console do 53 | ARGV.clear 54 | require 'irb' 55 | require 'leveldb' 56 | IRB.start 57 | end 58 | 59 | YARD::Rake::YardocTask.new(:doc) do |t| 60 | t.files = ['lib/**/*.rb'] 61 | t.options = ['--markup=markdown', 62 | '--no-private', 63 | '--markup-provider=redcarpet'] 64 | end 65 | -------------------------------------------------------------------------------- /test/test_db.rb: -------------------------------------------------------------------------------- 1 | require_relative './helper' 2 | 3 | class TestBasic < Minitest::Test 4 | attr_reader :db 5 | 6 | def setup 7 | @db ||= LevelDB::DB.new './tmp/test-db' 8 | end 9 | 10 | def teardown 11 | db.close 12 | db.destroy 13 | end 14 | 15 | def test_open 16 | assert_raises(LevelDB::DB::Error) do 17 | LevelDB::DB.new './doesnt-exist/foo' 18 | end 19 | assert db 20 | end 21 | 22 | def test_put 23 | foo = db.put(:foo, :bar) 24 | assert_equal 'bar', foo 25 | 26 | foo = db[:foo] = 'bax' 27 | assert_equal 'bax', foo 28 | end 29 | 30 | def test_read 31 | db.put(:foo, :bar) 32 | assert_equal 'bar', db.get(:foo) 33 | 34 | db[:foo] = 'bax' 35 | assert_equal 'bax', db[:foo] 36 | end 37 | 38 | def test_exists? 39 | db[:foo] = :bar 40 | 41 | assert db.exists?(:foo) 42 | assert db.includes?(:foo) 43 | assert db.contains?(:foo) 44 | assert db.member?(:foo) 45 | assert db.has_key?(:foo) 46 | refute db.exists?(:foxy) 47 | end 48 | 49 | def test_fetch 50 | db[:foo] = :bar 51 | 52 | assert_equal 'bar', db.fetch(:foo) 53 | assert_raises LevelDB::DB::KeyError do 54 | db.fetch(:sten) 55 | end 56 | assert_equal 'smith', db.fetch(:sten, :smith) 57 | assert_equal 'francine', db.fetch(:francine){ |key| key } 58 | end 59 | 60 | def test_delete 61 | db[:foo] = 'bar' 62 | 63 | res = db.delete(:foo) 64 | assert_equal 'bar', res 65 | 66 | res = db.delete(:foo) 67 | assert_nil res 68 | end 69 | 70 | def test_close 71 | d = LevelDB::DB.new './tmp/test-close' 72 | assert d.close 73 | assert_raises(LevelDB::DB::ClosedError){ d[:foo] } 74 | end 75 | 76 | def test_destroy 77 | d = LevelDB::DB.new './tmp/test-close' 78 | assert_raises(LevelDB::DB::Error){ d.destroy } 79 | assert d.close 80 | assert d.destroy 81 | end 82 | 83 | def test_stats 84 | assert_match /Level/, db.stats 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /benchmark/leveldb.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'leveldb' 3 | require 'benchmark' 4 | require 'minitest' 5 | 6 | puts '## Please wait, I\'m generating 100mb of random data ...' 7 | 8 | N = 10_240 9 | SAMPLE = [] 10 | 11 | File.open('/dev/urandom', File::RDONLY || File::NONBLOCK || File::NOCTTY) do |f| 12 | N.times { |i| SAMPLE << f.readpartial(5_120).unpack("H*")[0] } 13 | end 14 | 15 | db = LevelDB::DB.new '/tmp/bench', compression: false 16 | db.clear! 17 | 18 | puts '## Without compression:' 19 | Benchmark.bm do |x| 20 | x.report('put') { N.times { |i| db.put(i, SAMPLE[i]) } } 21 | x.report('get') { N.times { |i| raise unless db.get(i) == SAMPLE[i] } } 22 | end 23 | 24 | db.reopen! 25 | puts db.stats 26 | puts 27 | 28 | db.close; db.destroy 29 | db = LevelDB::DB.new '/tmp/bench', bloom_filter_bits_per_key: 100 30 | db.clear! 31 | 32 | puts '## With bloom filter @ 100 bits/key:' 33 | Benchmark.bm do |x| 34 | x.report('put') { N.times { |i| db.put(i, SAMPLE[i]) } } 35 | x.report('get') { N.times { |i| raise unless db.get(i) == SAMPLE[i] } } 36 | end 37 | 38 | db.reopen! 39 | puts db.stats 40 | puts 41 | 42 | db.close; db.destroy 43 | db = LevelDB::DB.new '/tmp/bench', compression: true 44 | db.clear! 45 | 46 | puts '## With compression:' 47 | Benchmark.bm do |x| 48 | x.report('put') { N.times { |i| db.put(i, SAMPLE[i]) } } 49 | x.report('get') { N.times { |i| raise unless db.get(i) == SAMPLE[i] } } 50 | end 51 | 52 | db.reopen! 53 | puts db.stats 54 | puts 55 | 56 | db.close; db.destroy 57 | db = LevelDB::DB.new '/tmp/bench', compression: true, bloom_filter_bits_per_key: 100 58 | db.clear! 59 | 60 | puts '## With compression and bloom filter @ 100 bits/key:' 61 | Benchmark.bm do |x| 62 | x.report('put') { N.times { |i| db.put(i, SAMPLE[i]) } } 63 | x.report('get') { N.times { |i| raise unless db.get(i) == SAMPLE[i] } } 64 | end 65 | 66 | db.reopen! 67 | puts db.stats 68 | puts 69 | 70 | db.close; db.destroy 71 | db = LevelDB::DB.new '/tmp/bench', compression: true 72 | db.clear! 73 | 74 | puts '## With batch:' 75 | Benchmark.bm do |x| 76 | x.report 'put' do 77 | db.batch do |batch| 78 | N.times { |i| batch.put(i, SAMPLE[i]) } 79 | end 80 | end 81 | end 82 | 83 | db.reopen! 84 | puts db.stats 85 | puts 86 | -------------------------------------------------------------------------------- /test/test_iterator.rb: -------------------------------------------------------------------------------- 1 | require_relative './helper' 2 | 3 | class TestIterator < Minitest::Test 4 | attr_reader :db 5 | 6 | def setup 7 | @db ||= LevelDB::DB.new './tmp/test-iterator' 8 | end 9 | 10 | def teardown 11 | db.close 12 | db.destroy 13 | end 14 | 15 | def test_next 16 | db[:a] = :sten 17 | db[:b] = :roger 18 | 19 | iterator = db.each 20 | 21 | assert_equal %w[a sten], iterator.next 22 | assert_equal %w[b roger], iterator.next 23 | 24 | assert db.each.next 25 | refute iterator.next 26 | end 27 | 28 | def test_reverse_next 29 | db[:a] = :sten 30 | db[:b] = :roger 31 | 32 | iterator = db.reverse_each 33 | 34 | assert_equal %w[b roger], iterator.next 35 | assert_equal %w[a sten], iterator.next 36 | 37 | assert db.each.next 38 | refute iterator.next 39 | end 40 | 41 | def test_range_next 42 | ('a'..'z').each { |l| db[l] = l.upcase } 43 | 44 | range = db.range('b', 'd') 45 | 46 | assert_equal %w[b B], range.next 47 | assert_equal %w[c C], range.next 48 | assert_equal %w[d D], range.next 49 | 50 | refute range.next 51 | end 52 | 53 | def test_range_reverse_next 54 | ('a'..'z').each { |l| db[l] = l.upcase } 55 | 56 | range = db.reverse_range('b', 'd') 57 | 58 | assert_equal %w[d D], range.next 59 | assert_equal %w[c C], range.next 60 | assert_equal %w[b B], range.next 61 | 62 | refute range.next 63 | end 64 | 65 | def test_keys 66 | db[:a] = :sten 67 | db[:b] = :roger 68 | 69 | assert_equal %w[a b], db.keys 70 | end 71 | 72 | def test_values 73 | db[:a] = :sten 74 | db[:b] = :roger 75 | 76 | assert_equal %w[sten roger], db.values 77 | end 78 | 79 | def test_block 80 | db[:a] = :sten 81 | db[:b] = :roger 82 | 83 | keys, values = [], [] 84 | db.each { |k,v| keys.push(k); values.push(v) } 85 | 86 | assert_equal %w[a b], keys 87 | assert_equal %w[sten roger], values 88 | end 89 | 90 | def test_reverse_block 91 | db[:a] = :sten 92 | db[:b] = :roger 93 | 94 | keys, values = [], [] 95 | db.reverse_each { |k,v| keys.push(k); values.push(v) } 96 | 97 | assert_equal %w[b a], keys 98 | assert_equal %w[roger sten], values 99 | end 100 | 101 | def test_range_block 102 | ('a'..'z').each { |l| db[l] = l.upcase } 103 | 104 | keys, values = [], [] 105 | db.range('b', 'd') { |k,v| keys.push(k); values.push(v) } 106 | 107 | assert_equal %w[b c d], keys 108 | assert_equal %w[B C D], values 109 | end 110 | 111 | def test_range_reverse_block 112 | ('a'..'z').each { |l| db[l] = l.upcase } 113 | 114 | keys, values = [], [] 115 | db.reverse_range('b', 'd') { |k,v| keys.push(k); values.push(v) } 116 | 117 | assert_equal %w[d c b], keys 118 | assert_equal %w[D C B], values 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/leveldb/iterator.rb: -------------------------------------------------------------------------------- 1 | module LevelDB 2 | class Iterator 3 | def initialize(db, read_opts, read_len, reverse=false) 4 | @_db = db 5 | @_read_opts = read_opts 6 | @_read_len = read_len 7 | @_reverse = reverse 8 | @_err = C::Pointer.malloc(C::SIZEOF_VOIDP) 9 | @_err.free = C[:free] 10 | @_iterator = C.create_iterator(@_db, @_read_opts) 11 | # @_iterator.free = C[:iter_destroy] 12 | rewind 13 | end 14 | 15 | def rewind 16 | if reverse? 17 | C.iter_seek_to_last(@_iterator) 18 | else 19 | C.iter_seek_to_first(@_iterator) 20 | end 21 | end 22 | 23 | def reverse_each(&block) 24 | @_reverse = !@_reverse 25 | rewind 26 | each(&block) 27 | end 28 | 29 | def each(&block) 30 | return self unless block_given? 31 | if current = self.next 32 | block[*current] 33 | end while valid? 34 | @_range = nil 35 | end 36 | 37 | def range(from, to, &block) 38 | @_range = [from.to_s, to.to_s].sort 39 | @_range = @_range.reverse if reverse? 40 | C.iter_seek(@_iterator, @_range.first, @_range.first.bytesize) if !reverse? 41 | 42 | each(&block) 43 | end 44 | 45 | def next 46 | while valid? && !in_range? 47 | move_next 48 | end if range? 49 | 50 | key, val = current 51 | 52 | return unless key 53 | 54 | [key, val] 55 | ensure 56 | move_next 57 | end 58 | 59 | def peek 60 | self.next 61 | ensure 62 | move_prev 63 | end 64 | 65 | def valid? 66 | C.iter_valid(@_iterator) == 1 67 | end 68 | 69 | def reverse? 70 | @_reverse 71 | end 72 | 73 | def range? 74 | !!@_range 75 | end 76 | 77 | def inspect 78 | "#" 79 | end 80 | alias to_s inspect 81 | 82 | private 83 | def current 84 | return unless valid? 85 | 86 | key = C.iter_key(@_iterator, @_read_len).to_s(@_read_len.value) 87 | val = C.iter_value(@_iterator, @_read_len).to_s(@_read_len.value) 88 | 89 | [key, val] 90 | end 91 | 92 | def in_range? 93 | reverse? ? (current[0] <= @_range[0] && current[0] >= @_range[1]) : 94 | (current[0] >= @_range[0] && current[0] <= @_range[1]) 95 | end 96 | 97 | def move_next 98 | return unless valid? 99 | 100 | if reverse? 101 | C.iter_prev(@_iterator) 102 | else 103 | C.iter_next(@_iterator) 104 | end 105 | end 106 | 107 | def move_prev 108 | return unless valid? 109 | 110 | if reverse? 111 | C.iter_next(@_iterator) 112 | else 113 | C.iter_prev(@_iterator) 114 | end 115 | end 116 | 117 | def errors? 118 | C.iter_get_error(@_iterator, @_err) 119 | !@_err.ptr.null? 120 | end 121 | 122 | def error_message 123 | return unless errors? 124 | @_err.ptr.to_s 125 | ensure 126 | if errors? 127 | @_err = C::Pointer.malloc(C::SIZEOF_VOIDP) 128 | @_err.free = C[:free] 129 | end 130 | end 131 | alias clear_errors! error_message 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/native.rb: -------------------------------------------------------------------------------- 1 | require 'fiddler' 2 | 3 | module LevelDB 4 | module Native 5 | include Fiddler 6 | 7 | LIBPATHS.push File.expand_path('../../ext/leveldb', __FILE__) 8 | 9 | prefix 'leveldb_' 10 | dlload 'libleveldb' 11 | 12 | cdef :open, VOIDP, options: VOIDP, name: VOIDP, errptr: VOIDP 13 | cdef :put, VOID, db: VOIDP, options: VOIDP, key: VOIDP, keylen: ULONG, val: VOIDP, vallen: ULONG, errptr: VOIDP 14 | cdef :get, VOIDP, db: VOIDP, options: VOIDP, key: VOIDP, keylen: ULONG, vallen: VOIDP, errptr: VOIDP 15 | cdef :delete, VOID, db: VOIDP, options: VOIDP, key: VOIDP, keylen: ULONG, errptr: VOIDP 16 | cdef :destroy_db, VOID, options: VOIDP, name: VOIDP, errptr: VOIDP 17 | cdef :repair_db, VOID, options: VOIDP, name: VOIDP, errptr: VOIDP 18 | cdef :release_snapshot, VOID, db: VOIDP, snapshot: VOIDP 19 | cdef :create_snapshot, VOIDP, db: VOIDP 20 | cdef :approximate_sizes, VOID, db: VOIDP, num_ranges: INT, range_start_key: VOIDP, range_start_key_len: VOIDP, range_limit_key: VOIDP, range_limit_key_len: VOIDP, sizes: VOIDP 21 | cdef :close, VOID, db: VOIDP 22 | cdef :property_value, VOIDP, db: VOIDP, propname: VOIDP 23 | cdef :compact_range, VOID, db: VOIDP, start_key: VOIDP, start_key_len: ULONG, limit_key: VOIDP, limit_key_len: ULONG 24 | cdef :free, VOID, ptr: VOIDP 25 | 26 | cdef :create_iterator, VOIDP, db: VOIDP, options: VOIDP 27 | cdef :iter_destroy, VOID, iterator: VOIDP 28 | cdef :iter_seek_to_first, VOID, iterator: VOIDP 29 | cdef :iter_seek, VOID, iterator: VOIDP, k: VOIDP, klen: ULONG 30 | cdef :iter_prev, VOID, iterator: VOIDP 31 | cdef :iter_key, VOIDP, iterator: VOIDP, klen: VOIDP 32 | cdef :iter_value, VOIDP, iterator: VOIDP, vlen: VOIDP 33 | cdef :iter_get_error, VOID, iterator: VOIDP, errptr: VOIDP 34 | cdef :iter_valid, UCHAR, iterator: VOIDP 35 | cdef :iter_next, VOID, iterator: VOIDP 36 | cdef :iter_seek_to_last, VOID, iterator: VOIDP 37 | 38 | cdef :writebatch_create, VOIDP 39 | cdef :writebatch_clear, VOID, writebatch: VOIDP 40 | cdef :writebatch_put, VOID, writebatch: VOIDP, key: VOIDP, klen: ULONG, val: VOIDP, vlen: ULONG 41 | cdef :writebatch_delete, VOID, writebatch: VOIDP, key: VOIDP, klen: ULONG 42 | cdef :writebatch_destroy, VOID, writebatch: VOIDP 43 | cdef :writebatch_iterate, VOID, writebatch: VOIDP, state: VOIDP, put: VOIDP, deleted: VOIDP 44 | cdef :write, VOID, db: VOIDP, options: VOIDP, batch: VOIDP, errptr: VOIDP 45 | 46 | cdef :options_create, VOIDP 47 | cdef :options_set_comparator, VOID, options: VOIDP, comparator: VOIDP 48 | cdef :options_set_create_if_missing, VOID, options: VOIDP, u_char: UCHAR 49 | cdef :options_set_paranoid_checks, VOID, options: VOIDP, u_char: UCHAR 50 | cdef :options_set_info_log, VOID, options: VOIDP, logger: VOIDP 51 | cdef :options_set_max_open_files, VOID, options: VOIDP, int: INT 52 | cdef :options_set_block_size, VOID, options: VOIDP, u_long: ULONG 53 | cdef :options_set_compression, VOID, options: VOIDP, int: INT 54 | cdef :options_set_filter_policy, VOID, options: VOIDP, filterpolicy: VOIDP 55 | cdef :options_set_env, VOID, options: VOIDP, env: VOIDP 56 | cdef :options_set_cache, VOID, options: VOIDP, cache: VOIDP 57 | cdef :options_set_error_if_exists, VOID, options: VOIDP, u_char: UCHAR 58 | cdef :options_set_block_restart_interval, VOID, options: VOIDP, int: INT 59 | cdef :options_set_write_buffer_size, VOID, options: VOIDP, u_long: ULONG 60 | cdef :options_destroy, VOID, options: VOIDP 61 | 62 | cdef :readoptions_create, VOIDP 63 | cdef :readoptions_destroy, VOID, readoptions: VOIDP 64 | cdef :readoptions_set_verify_checksums, VOID, readoptions: VOIDP, u_char: UCHAR 65 | cdef :readoptions_set_snapshot, VOID, readoptions: VOIDP, snapshot: VOIDP 66 | cdef :readoptions_set_fill_cache, VOID, readoptions: VOIDP, u_char: UCHAR 67 | 68 | cdef :cache_create_lru, VOIDP, capacity: ULONG 69 | cdef :cache_destroy, VOID, cache: VOIDP 70 | 71 | cdef :create_default_env, VOIDP 72 | cdef :env_destroy, VOID, env: VOIDP 73 | 74 | cdef :comparator_create, VOIDP, state: VOIDP, destructor: VOIDP, compare: VOIDP, name: VOIDP 75 | cdef :comparator_destroy, VOID, comparator: VOIDP 76 | 77 | cdef :writeoptions_destroy, VOID, writeoptions: VOIDP 78 | cdef :writeoptions_set_sync, VOID, writeoptions: VOIDP, u_char: UCHAR 79 | cdef :writeoptions_create, VOIDP 80 | 81 | cdef :filterpolicy_create_bloom, VOIDP, bits_per_key: INT 82 | cdef :filterpolicy_create, VOIDP, state: VOIDP, destructor: VOIDP, create_filter: VOIDP, key_may_match: VOIDP, name: VOIDP 83 | cdef :filterpolicy_destroy, VOID, filterpolicy: VOIDP 84 | 85 | cdef :minor_version, INT 86 | cdef :major_version, INT 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/leveldb/db.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'leveldb/iterator' 3 | require 'leveldb/batch' 4 | require 'leveldb/snapshot' 5 | 6 | module LevelDB 7 | class DB 8 | include Enumerable 9 | 10 | class Error < StandardError; end 11 | class KeyError < StandardError; end 12 | class ClosedError < StandardError; end 13 | 14 | attr_reader :path, :options 15 | @@mutex = Mutex.new 16 | 17 | DEFAULT = { 18 | create_if_missing: true, 19 | error_if_exists: false, 20 | paranoid_checks: false, 21 | write_buffer_size: 4 << 20, 22 | block_size: 4096, 23 | max_open_files: 200, 24 | block_cache_size: 8 * (2 << 20), 25 | block_restart_interval: 16, 26 | compression: false, 27 | verify_checksums: false, 28 | fill_cache: true 29 | } 30 | 31 | def initialize(path, options={}) 32 | new!(path, options) 33 | end 34 | 35 | def new!(path, options={}) 36 | @_db_opts = C.options_create 37 | @_write_opts = C.writeoptions_create 38 | @_read_opts = C.readoptions_create 39 | @_read_len = C.value('size_t') 40 | 41 | @options = DEFAULT.merge(options) 42 | 43 | @_cache = C.cache_create_lru(@options[:block_cache_size]) 44 | 45 | C.readoptions_set_verify_checksums(@_read_opts, @options[:verify_checksums] ? 1 : 0) 46 | C.readoptions_set_fill_cache(@_read_opts, @options[:fill_cache] ? 1 : 0) 47 | 48 | C.options_set_create_if_missing(@_db_opts, @options[:create_if_missing] ? 1 : 0) 49 | C.options_set_error_if_exists(@_db_opts, @options[:error_if_exists] ? 1 : 0) 50 | C.options_set_paranoid_checks(@_db_opts, @options[:paranoid_checks] ? 1 : 0) 51 | C.options_set_write_buffer_size(@_db_opts, @options[:write_buffer_size]) 52 | C.options_set_block_size(@_db_opts, @options[:block_size]) 53 | C.options_set_cache(@_db_opts, @_cache) 54 | C.options_set_max_open_files(@_db_opts, @options[:max_open_files]) 55 | C.options_set_block_restart_interval(@_db_opts, @options[:block_restart_interval]) 56 | C.options_set_compression(@_db_opts, @options[:compression] ? 1 : 0) 57 | 58 | if @options[:bloom_filter_bits_per_key] 59 | C.options_set_filter_policy(@_db_opts, C.filterpolicy_create_bloom(@options[:bloom_filter_bits_per_key])) 60 | end 61 | 62 | @_db_opts.free = @_write_opts.free = @_read_opts.free = C[:options_destroy] 63 | 64 | @path = path 65 | 66 | @_err = C::Pointer.malloc(C::SIZEOF_VOIDP) 67 | @_err.free = @_read_len.to_ptr.free = C[:free] 68 | 69 | @_db = C.open(@_db_opts, @path, @_err) 70 | @_db.free = C[:close] 71 | 72 | raise Error, error_message if errors? 73 | end 74 | private :new! 75 | 76 | def reopen 77 | close unless closed? 78 | @@mutex.synchronize { @_closed = false } 79 | new!(@path, @options) 80 | end 81 | alias reopen! reopen 82 | 83 | def []=(key, val) 84 | raise ClosedError if closed? 85 | 86 | key = key.to_s 87 | val = val.to_s 88 | 89 | C.put(@_db, @_write_opts, key, key.bytesize, val, val.bytesize, @_err) 90 | 91 | raise Error, error_message if errors? 92 | 93 | val 94 | end 95 | alias put []= 96 | 97 | def [](key) 98 | raise ClosedError if closed? 99 | 100 | key = key.to_s 101 | val = C.get(@_db, @_read_opts, key, key.bytesize, @_read_len, @_err) 102 | val.free = C[:free] 103 | 104 | raise Error, error_message if errors? 105 | 106 | @_read_len.value == 0 ? nil : val.to_s(@_read_len.value).clone 107 | end 108 | alias get [] 109 | 110 | def delete(key) 111 | raise ClosedError if closed? 112 | 113 | key = key.to_s 114 | val = get(key) 115 | C.delete(@_db, @_write_opts, key, key.bytesize, @_err) 116 | 117 | raise Error, error_message if errors? 118 | 119 | val 120 | end 121 | 122 | def exists?(key) 123 | get(key) != nil 124 | end 125 | alias includes? exists? 126 | alias contains? exists? 127 | alias member? exists? 128 | alias has_key? exists? 129 | 130 | def fetch(key, default=nil, &block) 131 | val = get(key) 132 | 133 | return val if val 134 | raise KeyError if default.nil? && !block_given? 135 | 136 | val = block_given? ? block[key] : default 137 | put(key, val) 138 | end 139 | 140 | def snapshot 141 | Snapshot.new(@_db, @_read_opts) 142 | end 143 | 144 | def batch(&block) 145 | raise ClosedError if closed? 146 | 147 | batch = Batch.new(@_db, @_write_opts) 148 | 149 | if block_given? 150 | block[batch] 151 | batch.write! 152 | else 153 | batch 154 | end 155 | end 156 | 157 | def close 158 | raise ClosedError if closed? 159 | 160 | # Prevent double free, I can't free it since 161 | # after this call we can still `destroy` it. 162 | @_db.free = nil 163 | C.close(@_db) 164 | @@mutex.synchronize { @_closed = true } 165 | raise Error, error_message if errors? 166 | 167 | true 168 | end 169 | 170 | def each(&block) 171 | raise ClosedError if closed? 172 | 173 | iterator = Iterator.new(@_db, @_read_opts, @_read_len) 174 | iterator.each(&block) 175 | iterator 176 | end 177 | 178 | def reverse_each(&block) 179 | each.reverse_each(&block) 180 | end 181 | 182 | def range(from, to, &block) 183 | each.range(from, to, &block) 184 | end 185 | 186 | def reverse_range(from, to, &block) 187 | reverse_each.range(from, to, &block) 188 | end 189 | 190 | def keys 191 | map { |k, v| k } 192 | end 193 | 194 | def values 195 | map { |k, v| v } 196 | end 197 | 198 | def closed? 199 | @@mutex.synchronize { @_closed } 200 | end 201 | 202 | def destroy 203 | C.destroy_db(@_db_opts, @path, @_err) 204 | raise Error, error_message if errors? 205 | 206 | true 207 | end 208 | 209 | def destroy! 210 | close && destroy && reopen 211 | end 212 | alias clear! destroy! 213 | 214 | def read_property(name) 215 | raise ClosedError if closed? 216 | 217 | C.property_value(@_db, name).to_s 218 | end 219 | 220 | def stats 221 | read_property('leveldb.stats') 222 | end 223 | 224 | def errors? 225 | return unless @_err 226 | !@_err.ptr.null? 227 | end 228 | 229 | def error_message 230 | return unless errors? 231 | @_err.ptr.to_s 232 | ensure 233 | if errors? 234 | @_err = C::Pointer.malloc(C::SIZEOF_VOIDP) 235 | @_err.free = C[:free] 236 | end 237 | end 238 | alias clear_errors! error_message 239 | 240 | def inspect 241 | "#" 242 | end 243 | alias to_s inspect 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leveldb 2 | 3 | LevelDB is a database library (C++, 350 kB) written at Google. It is an 4 | embedded database. LevelDB is a persistent ordered map. 5 | 6 | > LevelDB stores keys and values in arbitrary byte arrays, and data is sorted by 7 | > key. It supports batching writes, forward and backward iteration, and 8 | > compression of the data via Google's Snappy compression library. Still, 9 | > LevelDB is not a SQL database. (Wikipedia) 10 | 11 | ## Features 12 | 13 | * Keys and values are arbitrary byte arrays. 14 | * Data is stored **sorted** by key. 15 | * Callers can provide a (_soon_) **custom comparison** function to override the sort order. 16 | * The basic operations are Put(key,value), Get(key), Delete(key). 17 | * Multiple changes can be made in one **atomic batch**. 18 | * Users can create a **transient snapshot** to get a consistent view of data. 19 | * _Forward_ and _backward_ iteration is supported over the data. 20 | * Data is automatically **compressed** using the **Snappy** compression library. 21 | * External activity (file system operations etc.) is relayed through a virtual 22 | interface so users can customize the operating system interactions. 23 | * Detailed documentation about how to use the library is included 24 | with the [source code](http://code.google.com/p/leveldb/). 25 | 26 | ## Reading 27 | 28 | * [LevelDB](http://code.google.com/p/leveldb/) 29 | * [Great Reading](http://skipperkongen.dk/2013/02/14/having-a-look-at-leveldb/) 30 | * [Website](http://daddye.it/leveldb) 31 | 32 | ## Installation 33 | 34 | ### Development 35 | 36 | $ brew install snappy 37 | $ git clone git://github.com/DAddYE/leveldb.git 38 | $ cd leveldb 39 | $ bundle install 40 | $ bundle exec rake compile 41 | $ bundle exec rake console 42 | 43 | ### Standard 44 | 45 | $ brew install snappy 46 | $ gem install leveldb 47 | $ irb -r leveldb 48 | 49 | ## Usage 50 | 51 | Here a basic usage: 52 | 53 | ```rb 54 | db = LevelDB::DB.new '/tmp/foo' 55 | 56 | # Writing 57 | db.put('hello', 'world') 58 | db['hello'] = 'world' 59 | 60 | # Reading 61 | db.get('hello') # => world 62 | db['hello'] # => world 63 | db.exists?('hello') # => true 64 | 65 | # Reading/Writing 66 | db.fetch('hello', 'hello world') # => will write 'hello world' if there is no key 'hello' 67 | db.fetch('hello'){ |key| 'hello world' } # => same as above 68 | 69 | # Deleting 70 | db.delete('hello') 71 | 72 | # Iterating 73 | db.each { |key, val| puts "Key: #{key}, Val: #{val}" } 74 | db.reverse_each { |key, val| puts "Key: #{key}, Val: #{val}" } 75 | db.keys 76 | db.values 77 | db.map { |k,v| do_some_with(k, v) } 78 | db.reduce([]) { |memo, (k, v)| memo << k + v; memo } 79 | db.each # => enumerator 80 | db.reverse_each # => enumerator 81 | 82 | # Ranges 83 | db.range('c', 'd') { |k,v| do_some_with_only_keys_in_range } 84 | db.reverse_range('c', 'd') # => same as above but results are in reverse order 85 | db.range(...) # => enumerable 86 | 87 | # Batches 88 | db.batch do |b| 89 | b.put 'a', 1 90 | b.put 'b', 2 91 | b.delete 'c' 92 | end 93 | 94 | b = db.batch 95 | b.put 'a', 1 96 | b.put 'b', 2 97 | b.delete 'c' 98 | b.write! 99 | 100 | # Snapshots 101 | db.put 'a', 1 102 | db.put 'b', 2 103 | db.put 'c', 3 104 | 105 | snap = db.snapshot 106 | 107 | db.delete 'a' 108 | db.get 'a' # => nil 109 | 110 | snap.set! 111 | 112 | db.get('a') # => 1 113 | 114 | snap.reset! 115 | 116 | db.get('a') # => nil 117 | 118 | snap.set! 119 | 120 | db.get('a') # => 1 121 | 122 | # Properties 123 | db.read_property('leveldb.stats') 124 | 125 | # Level Files Size(MB) Time(sec) Read(MB) Write(MB) 126 | # -------------------------------------------------- 127 | # 0 1 0 0 0 0 128 | # 1 1 0 0 0 0 129 | 130 | # same of: 131 | db.stats 132 | ``` 133 | 134 | ## Benchmarks 135 | 136 | _Preface_: those are only for general purpose, I know that [zedshaw](http://zedshaw.com/essays/programmer_stats.html) 137 | will kill me for this, but ... on my mac: 138 | 139 | Model Identifier: MacBookPro10,1 140 | Processor Name: Intel Core i7 141 | Processor Speed: 2.3 GHz 142 | Number of Processors: 1 143 | Total Number of Cores: 4 144 | L2 Cache (per Core): 256 KB 145 | L3 Cache: 6 MB 146 | Memory: 8 GB 147 | 148 | The benchmark code is in [benchmark/leveldb.rb](/benchmark/leveldb.rb) 149 | 150 | Writing/Reading `100mb` of _very_ random data of `10kb` each: 151 | 152 | ### Without compression: 153 | 154 | user system total real 155 | put 0.530000 0.310000 0.840000 ( 1.420387) 156 | get 0.800000 0.460000 1.260000 ( 2.626631) 157 | 158 | Level Files Size(MB) Time(sec) Read(MB) Write(MB) 159 | -------------------------------------------------- 160 | 0 1 0 0 0 0 161 | 2 50 98 0 0 0 162 | 3 1 2 0 0 0 163 | 164 | ### With compression: 165 | 166 | user system total real 167 | put 0.850000 0.320000 1.170000 ( 1.721609) 168 | get 1.160000 0.480000 1.640000 ( 2.703543) 169 | 170 | Level Files Size(MB) Time(sec) Read(MB) Write(MB) 171 | -------------------------------------------------- 172 | 0 1 0 0 0 0 173 | 1 5 10 0 0 0 174 | 2 45 90 0 0 0 175 | 176 | **NOTE**: as you can see `snappy` can't compress that kind of _very very_ 177 | random data, but I was not interested to bench snappy (as a compressor) but 178 | only to see how (eventually) much _slower_ will be using it. As you can see, 179 | only a _few_ and on normal _data_ the db size will be much much better! 180 | 181 | ### With batch: 182 | 183 | user system total real 184 | put 0.260000 0.170000 0.430000 ( 0.433407) 185 | 186 | Level Files Size(MB) Time(sec) Read(MB) Write(MB) 187 | -------------------------------------------------- 188 | 0 1 100 1 0 100 189 | 190 | 191 | ## Difference between a c++ pure ruby impl? 192 | 193 | This, again, only for general purpose, but I want to compare the `c++` implementation 194 | of [leveldb-ruby](https://github.com/wmorgan/leveldb-ruby) with this that use ffi. 195 | 196 | I'm aware that this lib is 1 year older, but for those who cares, the basic bench: 197 | 198 | user system total real 199 | put 0.440000 0.300000 0.740000 ( 1.363188) 200 | get 0.440000 0.440000 1.460000 ( 2.407274) 201 | 202 | ## Todo 203 | 204 | 1. Add pluggable serializers 205 | 2. Custom comparators 206 | 207 | ## Contributing 208 | 209 | 1. Fork it 210 | 2. Create your feature branch (`git checkout -b my-new-feature`) 211 | 3. Commit your changes (`git commit -am 'Add some feature'`) 212 | 4. Push to the branch (`git push origin my-new-feature`) 213 | 5. Create new Pull Request 214 | --------------------------------------------------------------------------------