Repository: ioquatix/relaxo Branch: main Commit: 60808b666243 Files: 29 Total size: 33.7 KB Directory structure: gitextract_rc85mw7x/ ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── documentation-coverage.yaml │ ├── documentation.yaml │ ├── rubocop.yaml │ ├── test-coverage.yaml │ ├── test-external.yaml │ └── test.yaml ├── .gitignore ├── .rubocop.yml ├── benchmarks/ │ └── performance.rb ├── config/ │ └── sus.rb ├── fixtures/ │ └── relaxo/ │ └── test_records.rb ├── gems.rb ├── lib/ │ ├── relaxo/ │ │ ├── changeset.rb │ │ ├── database.rb │ │ ├── dataset.rb │ │ ├── directory.rb │ │ ├── logger.rb │ │ └── version.rb │ └── relaxo.rb ├── license.md ├── readme.md ├── relaxo.gemspec ├── release.cert └── test/ ├── relaxo/ │ ├── changeset.rb │ ├── concurrency.rb │ ├── database.rb │ └── enumeration.rb └── relaxo.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab indent_size = 2 [*.{yml,yaml}] indent_style = space indent_size = 2 ================================================ FILE: .github/workflows/documentation-coverage.yaml ================================================ name: Documentation Coverage on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm COVERAGE: PartialSummary jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - name: Validate coverage timeout-minutes: 5 run: bundle exec bake decode:index:coverage lib ================================================ FILE: .github/workflows/documentation.yaml ================================================ name: Documentation on: push: branches: - main # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: permissions: contents: read pages: write id-token: write # Allow one concurrent deployment: concurrency: group: "pages" cancel-in-progress: true env: CONSOLE_OUTPUT: XTerm BUNDLE_WITH: maintenance jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - name: Installing packages run: sudo apt-get install wget - name: Generate documentation timeout-minutes: 5 run: bundle exec bake utopia:project:static --force no - name: Upload documentation artifact uses: actions/upload-pages-artifact@v3 with: path: docs deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{steps.deployment.outputs.page_url}} needs: generate steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/rubocop.yaml ================================================ name: RuboCop on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Run RuboCop timeout-minutes: 10 run: bundle exec rubocop ================================================ FILE: .github/workflows/test-coverage.yaml ================================================ name: Test Coverage on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm COVERAGE: PartialSummary jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "3.4" steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 5 run: bundle exec bake test - uses: actions/upload-artifact@v4 with: include-hidden-files: true if-no-files-found: error name: coverage-${{matrix.os}}-${{matrix.ruby}} path: .covered.db validate: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - uses: actions/download-artifact@v4 - name: Validate coverage timeout-minutes: 5 run: bundle exec bake covered:validate --paths */.covered.db \; ================================================ FILE: .github/workflows/test-external.yaml ================================================ name: Test External on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "3.1" - "3.2" - "3.3" - "3.4" steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test:external ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - "3.1" - "3.2" - "3.3" - "3.4" experimental: [false] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test ================================================ FILE: .gitignore ================================================ /.bundle/ /pkg/ /gems.locked /.covered.db /external /tmp ================================================ FILE: .rubocop.yml ================================================ AllCops: DisabledByDefault: true Layout/IndentationStyle: Enabled: true EnforcedStyle: tabs Layout/InitialIndentation: Enabled: true Layout/IndentationWidth: Enabled: true Width: 1 Layout/IndentationConsistency: Enabled: true EnforcedStyle: normal Layout/BlockAlignment: Enabled: true Layout/EndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/BeginEndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/ElseAlignment: Enabled: true Layout/DefEndAlignment: Enabled: true Layout/CaseIndentation: Enabled: true Layout/CommentIndentation: Enabled: true Layout/EmptyLinesAroundClassBody: Enabled: true Layout/EmptyLinesAroundModuleBody: Enabled: true Style/FrozenStringLiteralComment: Enabled: true Style/StringLiterals: Enabled: true EnforcedStyle: double_quotes ================================================ FILE: benchmarks/performance.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2017-2025, by Samuel Williams. require "benchmark/ips" if ENV["BENCHMARK"] require "ruby-prof" if ENV["PROFILE"] require "flamegraph" if ENV["FLAMEGRAPH"] describe "Relaxo Performance" do let(:database_path) {File.join(__dir__, "test")} let(:database) {Relaxo.connect(database_path)} if defined? Benchmark def benchmark(name = nil) Benchmark.ips do |benchmark| # Collect more data for benchmark: benchmark.time = 20 benchmark.warmup = 10 benchmark.report(name) do |i| yield i end benchmark.compare! end end elsif defined? RubyProf def benchmark(name) result = RubyProf.profile do yield 1000 end #result.eliminate_methods!([/^((?!Utopia).)*$/]) printer = RubyProf::FlatPrinter.new(result) printer.print($stderr, min_percent: 1.0) printer = RubyProf::GraphHtmlPrinter.new(result) filename = name.gsub("/", "_") + ".html" File.open(filename, "w") do |file| printer.print(file) end end elsif defined? Flamegraph def benchmark(name) filename = name.gsub("/", "_") + ".html" Flamegraph.generate(filename) do yield 1 end end else def benchmark(name) yield 1 end end before(:each) do FileUtils.rm_rf(database_path) end it "single transaction should be fast" do benchmark("single") do |iterations| database.commit(message: "Some Documents") do |dataset| iterations.times do |i| object = dataset.append("good-#{i}") dataset.write("#{i%100}/#{i}", object) end end end end it "multiple transactions should be fast" do benchmark("multiple") do |iterations| iterations.times do |i| database.commit(message: "Some Documents") do |dataset| object = dataset.append("good-#{i}") dataset.write("#{i%100}/#{i}", object) end end end end end ================================================ FILE: config/sus.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require "covered/sus" include Covered::Sus ================================================ FILE: fixtures/relaxo/test_records.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2017-2025, by Samuel Williams. require "relaxo" require "tmpdir" module Relaxo TemporaryDatabase = Sus::Shared("temporary database") do def around Dir.mktmpdir do |directory| @root = directory super end end let(:database_path) {@root} let(:database) {Relaxo.connect(database_path)} end TestRecords = Sus::Shared("test records") do include_context Relaxo::TemporaryDatabase let(:prefix) {"records"} def before super database.commit(message: "Create Sample Data") do |dataset| 20.times do |i| object = dataset.append("good-#{i}") dataset.write("#{prefix}/#{i}", object) end 10.times do |i| object = dataset.append("bad-#{i}") dataset.write("#{prefix}/subdirectory/#{i}", object) end end end end end ================================================ FILE: gems.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. source "https://rubygems.org" gemspec group :maintenance, optional: true do gem "bake-gem" gem "bake-modernize" gem "utopia-project" end group :test do gem "sus" gem "covered" gem "decode" gem "rubocop" gem "bake-test" gem "bake-test-external" gem "msgpack" end ================================================ FILE: lib/relaxo/changeset.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. require_relative "dataset" module Relaxo class Changeset < Dataset def initialize(repository, tree) super @changes = {} @directories = {} end attr :ref attr :changes def changes? @changes.any? end def read(path) if update = @changes[path] if update[:action] != :remove @repository.read(update[:oid]) end else super end end def append(data, type = :blob) oid = @repository.write(data, type) return @repository.read(oid) end def write(path, object, mode = 0100644) root, _, name = path.rpartition("/") entry = @changes[path] = { action: :upsert, oid: object.oid, object: object, filemode: mode, path: path, root: root, name: name, } fetch_directory(root).insert(entry) return entry end alias []= write def delete(path) root, _, name = path.rpartition("/") entry = @changes[path] = { action: :remove, path: path, root: root, name: name, } fetch_directory(root).delete(entry) return entry end def abort! throw :abort end def write_tree @tree.update(@changes.values) end end end ================================================ FILE: lib/relaxo/database.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. # Copyright, 2017, by Huba Nagy. require "rugged" require_relative "logger" require_relative "dataset" require_relative "changeset" module Relaxo HEAD = "HEAD".freeze class Database def initialize(path, branch, metadata = {}) @path = path @metadata = metadata @repository = Rugged::Repository.new(path) # @repository.config['core.fsyncObjectFiles'] = fsync @branch = branch end def config @repository.config end attr :path attr :metadata attr :repository # @attribute branch [String] The branch that this database is currently working with. attr :branch # Completely clear out the database. def clear! if head = @repository.branches[@branch] @repository.references.delete(head) end end def empty? @repository.empty? end def head @repository.branches[@branch] end def [] key @metadata[key] end # During the execution of the block, changes don't get stored immediately, so reading from the dataset (from outside the block) will continue to return the values that were stored in the configuration when the transaction was started. # @return the result of the block. def commit(**options) result = nil track_time(options[:message]) do catch(:abort) do begin parent, tree = latest_commit changeset = Changeset.new(@repository, tree) result = yield changeset end until apply(parent, changeset, **options) end end return result end # Efficient point-in-time read-only access. def current _, tree = latest_commit dataset = Dataset.new(@repository, tree) yield dataset if block_given? return dataset end # revision history of given object def history(path) head, _ = latest_commit walker = Rugged::Walker.new(@repository) # Sounds like 'Walker, Texas Ranger'... walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE) walker.push(head.oid) commits = [] old_oid = nil walker.each do |commit| dataset = Dataset.new(@repository, commit.tree) oid = dataset.read(path).oid if oid != old_oid # modified yield commit if block_given? commits << commit old_oid = oid end break if oid.nil? && !old_oid.nil? # deleted or moved end return commits end private def track_time(message) start_time = Time.now yield ensure end_time = Time.now elapsed_time = end_time - start_time Console.debug(self) {"#{message.inspect}: %0.3fs" % elapsed_time} end def apply(parent, changeset, **options) return true unless changeset.changes? options[:tree] = changeset.write_tree options[:parents] ||= [parent] options[:update_ref] ||= "refs/heads/#{@branch}" begin Rugged::Commit.create(@repository, options) rescue Rugged::ObjectError return false end end def latest_commit if head = self.head return head.target, head.target.tree else return nil, empty_tree end end def empty_tree @empty_tree ||= Rugged::Tree.empty(@repository) end end end ================================================ FILE: lib/relaxo/dataset.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. require "rugged" require_relative "directory" module Relaxo class Dataset def initialize(repository, tree) @repository = repository @tree = tree @directories = {} end def read(path) if entry = @tree.path(path) and entry[:type] == :blob and oid = entry[:oid] @repository.read(oid) end rescue Rugged::TreeError return nil end alias [] read def file? read(path) end def exist?(path) read(path) or directory?(path) end def directory?(path) @directories.key?(path) or @tree.path(path)[:type] == :tree rescue Rugged::TreeError return false end def each(path = "", &block) return to_enum(:each, path) unless block_given? directory = fetch_directory(path) directory.each(&block) end protected def fetch_directory(path) @directories[path] ||= Directory.new(@repository, @tree, path) end end end ================================================ FILE: lib/relaxo/directory.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2017-2025, by Samuel Williams. require "rugged" module Relaxo class Directory def initialize(repository, root_tree, path) @repository = repository # The root tree, which path is relative to: @root_tree = root_tree # The entry and tree for the directory itself: @entry = nil @tree = nil @path = path @entries = nil @changes = {} end def freeze @changes.freeze super end def entries @entries ||= load_entries! end def each(&block) return to_enum(:each) unless block_given? entries.each do |entry| entry[:object] ||= @repository.read(entry[:oid]) yield entry[:name], entry[:object] end end def each_entry(&block) return to_enum(:each_entry) unless block_given? entries.each(&block) end def insert(entry) _, _, name = entry[:name].rpartition("/") @changes[name] = entry # Blow away the cache: @entries = nil end def delete(entry) _, _, name = entry[:name].rpartition("/") @changes[name] = nil # Blow away the cache: @entries = nil end private # Look up the entry for the given directory `@path`: def fetch_entry @entry ||= @root_tree.path(@path) end # Load the directory tree for the given `@path`: def fetch_tree @tree ||= Rugged::Tree.new(@repository, fetch_entry[:oid]) rescue Rugged::TreeError return nil end # Load the entries from the tree, applying any changes. def load_entries! entries = @changes.dup if tree = fetch_tree tree.each_blob do |entry| unless entries.key? entry[:name] entries[entry[:name]] = entry end end end return entries.values.compact.sort_by{|entry| entry[:name]} end end end ================================================ FILE: lib/relaxo/logger.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require "console" module Relaxo extend Console end ================================================ FILE: lib/relaxo/version.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. module Relaxo VERSION = "1.8.0" end ================================================ FILE: lib/relaxo.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. require "relaxo/database" require "etc" require "socket" module Relaxo DEFAULT_BRANCH = "main".freeze def self.connect(path, branch: nil, sync: nil, create: true, **metadata) if !File.exist?(path) || create repository = Rugged::Repository.init_at(path, true) if branch repository.head = "refs/heads/#{branch}" end if sync || ENV["RELAXO_SYNC"] repository.config["core.fsyncObjectFiles"] = true end else repository = Rugged::Repository.new(path) end # Automatically detect the current branch if `branch` is not provided: branch ||= self.default_branch(repository) database = Database.new(path, branch, metadata) if config = database.config unless config["user.name"] login = Etc.getpwuid hostname = Socket.gethostname if login config["user.name"] = login.name config["user.email"] = "#{login.name}@#{hostname}" end end end return database end private # Detect the default branch of the repository, taking into account unborn branches. def self.default_branch(repository) if head = repository.references["HEAD"] if target_id = head.target_id return target_id.sub(/^refs\/heads\//, "") end end return DEFAULT_BRANCH end end ================================================ FILE: license.md ================================================ # MIT License Copyright, 2012-2025, by Samuel Williams. Copyright, 2017-2018, by Huba Nagy. Copyright, 2020, by Olle Jonsson. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: readme.md ================================================ # ![Relaxo](logo.svg) Relaxo is a transactional database built on top of git. It's aim is to provide a robust interface for document storage and sorted indexes. If you prefer a higher level interface, you can try [relaxo-model](https://github.com/ioquatix/relaxo-model). [![Development Status](https://github.com/ioquatix/relaxo/workflows/Test/badge.svg)](https://github.com/ioquatix/relaxo/actions?workflow=Test) ## Installation Add this line to your application's Gemfile: ``` ruby gem 'relaxo' ``` And then execute: $ bundle Or install it yourself as: $ gem install relaxo ## Usage Connect to a local database and manipulate some documents. ``` ruby require 'relaxo' require 'msgpack' DB = Relaxo.connect("test") DB.commit(message: "Create test data") do |dataset| object = dataset.append(MessagePack.dump({bob: 'dole'})) dataset.write("doc1.msgpack", object) end DB.commit(message: "Update test data") do |dataset| doc = MessagePack.load dataset.read('doc1.msgpack').data doc[:foo] = 'bar' object = dataset.append(MessagePack.dump(doc)) dataset.write("doc2.msgpack", object) end doc = MessagePack.load DB.current['doc2.msgpack'].data puts doc # => {"bob"=>"dole", "foo"=>"bar"} ``` ### Document Storage Relaxo uses the git persistent data structure for storing documents. This data structure exposes a file-system like interface, which stores any kind of data. This means that you are free to use JSON, or BSON, or MessagePack, or JPEG, or XML, or any combination of those. Relaxo has a transactional model for both reading and writing. #### Authors By default, Relaxo sets up the repository author using the login name and hostname of the current session. You can explicitly change this by modifying `database.config`. Additionally, you can set this per-commit: ``` ruby database.commit(message: "Testing Enumeration", author: {user: "Alice", email: "alice@localhost"}) do |dataset| object = dataset.append("Hello World!") dataset.write("hello.txt", object) end ``` #### Reading Files ``` ruby path = "path/to/document" DB.current do |dataset| object = dataset.read(path) puts "The object id: #{object.oid}" puts "The object data size: #{object.size}" puts "The object data: #{object.data.inspect}" end ``` #### Writing Files ``` ruby path = "path/to/document" data = MessagePack.dump(document) DB.commit(message: "Adding document") do |changeset| object = changeset.append(data) changeset.write(path, object) end ``` ### Datasets and Transactions `Dataset`s and `Changeset`s are important concepts. Relaxo doesn't allow arbitrary access to data, but instead exposes the git persistent model for both reading and writing. The implications of this are that when reading or writing, you always see a consistent snapshot of the data store. ### Suitability Relaxo is designed to scale to the hundreds of thousands of documents. It's designed around the git persistent data store, and therefore has some performance and concurrency limitations due to the underlying implementation. Because it maintains a full history of all changes, the repository would continue to grow over time by default, but there are mechanisms to deal with that. #### Performance Relaxo can do anywhere from 1000-10,000 inserts per second depending on how you structure the workload. Relaxo Performance Warming up -------------------------------------- single 129.000 i/100ms Calculating ------------------------------------- single 6.224k (±14.7%) i/s - 114.036k in 20.000025s single transaction should be fast Warming up -------------------------------------- multiple 152.000 i/100ms Calculating ------------------------------------- multiple 1.452k (±15.2%) i/s - 28.120k in 20.101831s multiple transactions should be fast Reading data is lighting fast as it's loaded directly from disk and cached. ### Loading Data As Relaxo is unapologetically based on git, you can use git directly with a non-bare working directory to add any files you like. You can even point Relaxo at an existing git repository. ### Durability Relaxo is based on `libgit2` and asserts that it is a transactional database. We base this assertion on: - All writes into the object store using `libgit2` are atomic and synchronized to disk. - All updates to refs are atomic and synchronized to disk. Provided these two invariants are maintained, the operation of Relaxo will be safe, even if there are unexpected interruptions to the program. The durability guarantees of Relaxo depend on [`libgit2` calling `fsync`](https://github.com/libgit2/libgit2/pull/4030), and [this being respected by the underlying hardware](http://www.evanjones.ca/intel-ssd-durability.html). Otherwise, durability cannot be guaranteed. ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. ### Developer Certificate of Origin In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. ### Community Guidelines This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. ================================================ FILE: relaxo.gemspec ================================================ # frozen_string_literal: true require_relative "lib/relaxo/version" Gem::Specification.new do |spec| spec.name = "relaxo" spec.version = Relaxo::VERSION spec.summary = "Relaxo is versioned document database built on top of git." spec.authors = ["Samuel Williams", "Huba Nagy", "Olle Jonsson"] spec.license = "MIT" spec.cert_chain = ["release.cert"] spec.signing_key = File.expand_path("~/.gem/release.pem") spec.homepage = "https://github.com/ioquatix/relaxo" spec.metadata = { "funding_uri" => "https://github.com/sponsors/ioquatix/", "source_code_uri" => "https://github.com/ioquatix/relaxo.git", } spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) spec.required_ruby_version = ">= 3.1" spec.add_dependency "console" spec.add_dependency "rugged" end ================================================ FILE: release.cert ================================================ -----BEGIN CERTIFICATE----- MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= -----END CERTIFICATE----- ================================================ FILE: test/relaxo/changeset.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2017-2025, by Samuel Williams. require "relaxo/test_records" describe Relaxo::Changeset do include_context Relaxo::TestRecords it "should enumerate all documents including writes" do records = [] database.commit(message: "Testing Enumeration") do |dataset| 5.times do |i| object = dataset.append("extra-#{i}") dataset.write("#{prefix}/extra-#{i}", object) end expect(dataset.exist?("#{prefix}/extra-0")).to be_truthy records = dataset.each(prefix).to_a end expect(records.count).to be == 25 end it "should enumerate all documents excluding deletes" do records = database.commit(message: "Testing Enumeration") do |dataset| 5.times do |i| dataset.delete("#{prefix}/#{i}") end expect(dataset.exist?("#{prefix}/0")).to be_falsey dataset.each(prefix).to_a end expect(records.count).to be == 15 end let(:author) do {name: "Testing McTestface", email: "testing@testing.com"} end it "can use specified author" do database.commit(message: "Testing Enumeration", author: author) do |dataset| object = dataset.append("Hello World!") dataset.write("hello.txt", object) end commit = database.head.target expect(commit.author).to have_keys( name: be == "Testing McTestface", email: be == "testing@testing.com", ) end end ================================================ FILE: test/relaxo/concurrency.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2017-2025, by Samuel Williams. require "relaxo/test_records" describe Relaxo::Changeset do include_context Relaxo::TestRecords it "should detect conflicts" do events = [] alice = Fiber.new do database.commit(message: "Alice Data") do |changeset| events << :alice object = changeset.append("sample-data-1") changeset.write("conflict-path", object) Fiber.yield end end bob = Fiber.new do database.commit(message: "Bob Data") do |changeset| events << :bob object = changeset.append("sample-data-1") changeset.write("conflict-path", object) Fiber.yield end end alice.resume bob.resume alice.resume bob.resume expect(events).to be == [:alice, :bob, :bob] end end ================================================ FILE: test/relaxo/database.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2025, by Samuel Williams. # Copyright, 2017, by Huba Nagy. require "relaxo" require "relaxo/test_records" describe Relaxo::Database do include_context Relaxo::TemporaryDatabase let(:document_path) {"test/document.json"} let(:sample_json) {"[1, 2, 3]"} it "should be initially empty" do expect(database).to be(:empty?) end it "prepares user details in config" do expect(database.config.to_hash).to have_keys( "user.name", "user.email" ) end it "can clear database" do database.clear! expect(database).to be(:empty?) end it "should not be empty with one document" do database.commit(message: "Create test document") do |dataset| oid = dataset.append(sample_json) dataset.write(document_path, oid) end expect(database).not.to be(:empty?) end it "should be able to clear the database" do database.commit(message: "Create test document") do |dataset| oid = dataset.append(sample_json) dataset.write(document_path, oid) end expect(database).not.to be(:empty?) database.clear! expect(database).to be(:empty?) end it "should have metadata" do expect(database.metadata).to be == {} end it "should create a document" do database.commit(message: "Create test document") do |dataset| oid = dataset.append(sample_json) dataset.write(document_path, oid) end database.current do |dataset| expect(dataset[document_path].data).to be == sample_json end end it "should erase a document" do database.commit(message: "Create test document") do |dataset| oid = dataset.append(sample_json) dataset.write(document_path, oid) end database.commit(message: "Delete test document") do |dataset| dataset.delete(document_path) end database.current do |dataset| expect(dataset[document_path]).to be_nil end end it "should create multiple documents" do database.commit(message: "Create first document") do |dataset| oid = dataset.append(sample_json) dataset.write(document_path, oid) end database.commit(message: "Create second document") do |dataset| oid = dataset.append(sample_json) dataset.write(document_path + "2", oid) end database.current do |dataset| expect(dataset[document_path].data).to be == sample_json expect(dataset[document_path + "2"].data).to be == sample_json end end it "can enumerate documents" do database.commit(message: "Create first document") do |dataset| oid = dataset.append(sample_json) 10.times do |id| dataset.write(document_path + "-#{id}", oid) end end database.current do |dataset| expect(dataset.each("test").count).to be == 10 end end it "can enumerate commit history of a document" do 10.times do |id| database.commit(message: "revising the document #{id}") do |changeset| oid = changeset.append("revision \##{id} of this document") changeset.write("test/doot.txt", oid) end end database.commit(message: "unrelated commit") do |changeset| oid = changeset.append("unrelated document") changeset.write("test/unrelated.txt", oid) end expect(database.history("test/doot.txt").count).to be == 10 end end ================================================ FILE: test/relaxo/enumeration.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2017-2025, by Samuel Williams. require "relaxo/test_records" describe Relaxo::Dataset do include_context Relaxo::TestRecords it "should enumerate all documents" do records = [] database.current do |dataset| records = dataset.each(prefix).to_a end expect(records.count).to be == 20 end end describe Relaxo::Changeset do include_context Relaxo::TestRecords it "should enumerate all documents" do records = [] database.commit(message: "Testing Enumeration") do |dataset| records = dataset.each(prefix).to_a end expect(records.count).to be == 20 end end ================================================ FILE: test/relaxo.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. require "relaxo/test_records" describe Relaxo do with ".connect" do include Relaxo::TemporaryDatabase it "can connect to a new database" do expect(database).to be_a Relaxo::Database expect(database.branch).to (be == "main").or(be == "master") end it "can connect to a new database with an alternative branch name" do Relaxo.connect(database_path, branch: "development") expect(database).to be_a Relaxo::Database expect(database.branch).to be == "development" end end end