├── .rspec ├── Gemfile ├── lib └── active_record │ ├── sequence │ ├── version.rb │ ├── error.rb │ └── sequence_sql_builder.rb │ └── sequence.rb ├── bin ├── setup └── console ├── Rakefile ├── .gitignore ├── gemfiles ├── rails_4.2.11.gemfile └── rails_5.2.2.gemfile ├── .rubocop.yml ├── .travis.yml ├── LICENSE.txt ├── active_record-sequence.gemspec ├── Appraisals ├── README.md └── spec ├── sequence_spec.rb └── spec_helper.rb /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_record-sequence.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/active_record/sequence/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Sequence 3 | VERSION = '0.3.0'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | psql -c 'create database travis_ci_test;' 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /gemfiles/*.gemfile.lock 11 | -------------------------------------------------------------------------------- /gemfiles/rails_4.2.11.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.2.11" 6 | gem "pg", "< 1.0.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.2.2" 6 | gem "pg", ">= 1.0.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'active_record/sequence' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /lib/active_record/sequence/error.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Sequence 3 | Error = Class.new(StandardError) 4 | # Sequence is already exists and thus could not be created. 5 | AlreadyExist = Class.new(Error) 6 | # To obtain current value, you have to call `#next` first. 7 | CurrentValueUndefined = Class.new(Error) 8 | # Sequence is not exists and thus could not be deleted or accessed. 9 | NotExist = Class.new(Error) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - gemfiles/**/* 4 | 5 | Metrics/LineLength: 6 | Max: 120 7 | 8 | Style/TrailingCommaInHashLiteral: 9 | EnforcedStyleForMultiline: comma 10 | 11 | Style/TrailingCommaInArrayLiteral: 12 | EnforcedStyleForMultiline: comma 13 | 14 | Style/TrailingCommaInArguments: 15 | EnforcedStyleForMultiline: comma 16 | 17 | Metrics/BlockLength: 18 | Exclude: 19 | - spec/**/*.rb 20 | 21 | Style/FormatStringToken: 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | services: 4 | - postgresql 5 | before_script: 6 | - bundle install 7 | - psql -c "create database travis_ci_test;" -U postgres 8 | matrix: 9 | include: 10 | - :rvm: 2.4.5 11 | :gemfile: gemfiles/rails_4.2.11.gemfile 12 | - :rvm: 2.5.3 13 | :gemfile: gemfiles/rails_4.2.11.gemfile 14 | - :rvm: 2.6.1 15 | :gemfile: gemfiles/rails_4.2.11.gemfile 16 | - :rvm: 2.4.5 17 | :gemfile: gemfiles/rails_5.2.2.gemfile 18 | - :rvm: 2.5.3 19 | :gemfile: gemfiles/rails_5.2.2.gemfile 20 | - :rvm: 2.6.1 21 | :gemfile: gemfiles/rails_5.2.2.gemfile 22 | script: 23 | - bundle exec rake spec 24 | - bundle exec rubocop --fail-level C 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Tëma Bolshakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /active_record-sequence.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'active_record/sequence/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'active_record-sequence' 7 | spec.version = ActiveRecord::Sequence::VERSION 8 | spec.authors = ['Tëma Bolshakov'] 9 | spec.email = ['tema@bolshakov.dev'] 10 | spec.license = "MIT" 11 | 12 | spec.summary = "Provide access to PostgreSQL's sequences" 13 | spec.description = "Provide access to PostgreSQL's sequences" 14 | spec.homepage = 'https://github.com/bolshakov/active_record-sequence' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = 'exe' 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_runtime_dependency 'activerecord', '>= 4.2.11', '<= 6.0.0.beta1' 22 | spec.add_runtime_dependency 'pg' 23 | 24 | spec.add_development_dependency 'appraisal', '2.1.0' 25 | spec.add_development_dependency 'bundler' 26 | spec.add_development_dependency 'rake', '11.3.0' 27 | spec.add_development_dependency 'rspec', '3.5.0' 28 | spec.add_development_dependency 'rubocop', '0.64.0' 29 | end 30 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | matrix = [ 4 | { 5 | rails_version: '4.2.11', 6 | ruby_versions: %w[2.4.5 2.5.3 2.6.1], 7 | pg_version: '< 1.0.0', 8 | }, 9 | { 10 | rails_version: '5.2.2', 11 | ruby_versions: %w[2.4.5 2.5.3 2.6.1], 12 | pg_version: '>= 1.0.0', 13 | }, 14 | ] 15 | 16 | matrix.each do |gemfile| 17 | rails_version = gemfile.fetch(:rails_version) 18 | pg_version = gemfile.fetch(:pg_version) 19 | 20 | appraise "rails_#{rails_version}" do 21 | gem 'activerecord', "~> #{rails_version}" 22 | gem 'pg', pg_version 23 | end 24 | end 25 | 26 | travis = ::YAML.dump( 27 | 'language' => 'ruby', 28 | 'services' => [ 29 | 'postgresql', 30 | ], 31 | 'before_script' => [ 32 | 'bundle install', 33 | 'psql -c "create database travis_ci_test;" -U postgres', 34 | ], 35 | 'matrix' => { 36 | 'include' => 37 | matrix.flat_map do |rails_version:, ruby_versions:, **| 38 | ruby_versions.map do |ruby_version| 39 | { 40 | rvm: ruby_version, 41 | gemfile: "gemfiles/rails_#{rails_version}.gemfile", 42 | } 43 | end 44 | end, 45 | }, 46 | 'script' => [ 47 | 'bundle exec rake spec', 48 | 'bundle exec rubocop --fail-level C', 49 | ], 50 | ) 51 | 52 | ::File.open('.travis.yml', 'w+') do |file| 53 | file.write(travis) 54 | end 55 | -------------------------------------------------------------------------------- /lib/active_record/sequence/sequence_sql_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Sequence # rubocop:disable Style/Documentation 3 | # Builds SQL statement for creating sequence 4 | # @api private 5 | class SequenceSQLBuilder 6 | attr_reader :options 7 | attr_reader :parts 8 | 9 | def initialize(name, options = {}) 10 | @options = options 11 | @parts = [format('CREATE SEQUENCE %s', name)] 12 | end 13 | 14 | def to_sql 15 | configure_increment 16 | configure_min_value 17 | configure_max_value 18 | configure_start_value 19 | configure_cycle 20 | parts.join(' ') 21 | end 22 | 23 | private 24 | 25 | def configure_increment 26 | parts << format('INCREMENT BY %s', options[:increment]) if options[:increment] 27 | end 28 | 29 | def configure_min_value 30 | parts << format('MINVALUE %s', options[:min]) if options[:min] 31 | end 32 | 33 | def configure_max_value 34 | parts << format('MAXVALUE %s', options[:max]) if options[:max] 35 | end 36 | 37 | def configure_start_value 38 | parts << format('START %s', options[:start]) if options[:start] 39 | end 40 | 41 | def configure_cycle 42 | parts << (options.fetch(:cycle, false) ? 'CYCLE' : 'NO CYCLE') 43 | end 44 | end 45 | 46 | private_constant(:SequenceSQLBuilder) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/bolshakov/active_record-sequence.svg?branch=master)](https://travis-ci.org/bolshakov/active_record-sequence) 2 | [![Gem Version](https://badge.fury.io/rb/active_record-sequence.svg)](https://badge.fury.io/rb/active_record-sequence) 3 | 4 | # ActiveRecord::Sequence 5 | 6 | Access to [PostgreSQL's Sequences](https://www.postgresql.org/docs/8.1/static/sql-createsequence.html) 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'active_record-sequence' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install active_record-sequence 23 | 24 | ## Usage 25 | 26 | By default new sequence starts from `1`: 27 | 28 | ```ruby 29 | sequence = ActiveRecord::Sequence.create('numbers') 30 | ``` 31 | 32 | `#next` returns next value in the sequence: 33 | 34 | ```ruby 35 | sequence.next #=> 1 36 | sequence.next #=> 2 37 | ``` 38 | 39 | `#peek` returns current value: 40 | 41 | ```ruby 42 | sequence.peek #=> 2 43 | ``` 44 | 45 | You can start a sequence with specific value: 46 | 47 | ```ruby 48 | sequence = ActiveRecord::Sequence.create('numbers', start: 42) 49 | sequence.next #=> 42 50 | sequence.next #=> 43 51 | ``` 52 | 53 | Specify custom increment value: 54 | 55 | ```ruby 56 | sequence = ActiveRecord::Sequence.create('numbers', increment: 3) 57 | sequence.next #=> 1 58 | sequence.next #=> 4 59 | ``` 60 | 61 | If you pass negative increment, a sequence will be decreasing: 62 | 63 | ```ruby 64 | sequence = ActiveRecord::Sequence.create('numbers', increment: -3) 65 | sequence.next #=> -1 66 | sequence.next #=> -4 67 | ``` 68 | 69 | To limit number of elements in a sequence specify `max` value: 70 | 71 | ```ruby 72 | sequence = ActiveRecord::Sequence.create('numbers', max: 2) 73 | sequence.next #=> 1 74 | sequence.next #=> 2 75 | sequence.next #=> fail with StopIteration 76 | ``` 77 | 78 | Decreasing sequence may be limited as well: 79 | 80 | ```ruby 81 | sequence = ActiveRecord::Sequence.create('numbers', min: -2, increment: -1) 82 | sequence.next #=> -1 83 | sequence.next #=> -2 84 | sequence.next #=> fail with StopIteration 85 | ``` 86 | 87 | To define infinite sequence, use `cycle` option: 88 | 89 | ```ruby 90 | sequence = ActiveRecord::Sequence.create('numbers', max: 2, cycle: true) 91 | sequence.next #=> 1 92 | sequence.next #=> 2 93 | sequence.next #=> 1 94 | sequence.next #=> 2 95 | # etc. 96 | ``` 97 | 98 | You can use previously created sequence by instantiating `Sequence` class: 99 | 100 | ```ruby 101 | ActiveRecord::Sequence.create('numbers', max: 2, cycle: true) 102 | sequence = ActiveRecord::Sequence.new('numbers') 103 | sequence.next #=> 1 104 | sequence.next #=> 2 105 | sequence.next #=> 1 106 | ``` 107 | 108 | To destroy a sequence: 109 | 110 | ```ruby 111 | ActiveRecord::Sequence.drop('numbers') 112 | ``` 113 | 114 | ## Development 115 | 116 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests. 117 | You can also run `bin/console` for an interactive prompt that will allow you to experiment. 118 | 119 | To install this gem onto your local machine, run `bundle exec rake install`. To release a 120 | new version, update the version number in `version.rb`, and then run `bundle exec rake release`, 121 | which will create a git tag for the version, push git commits and tags, and push the `.gem` 122 | file to [rubygems.org](https://rubygems.org). 123 | 124 | 125 | We test this gem against different versions of `ActiveRecord` using [appraisal](https://github.com/thoughtbot/appraisal) gem. 126 | To regenerate gemfiles run: 127 | 128 | $ appraisal install 129 | 130 | To run specs against all versions: 131 | 132 | $ appraisal rake spec 133 | 134 | ## Contributing 135 | 136 | Bug reports and pull requests are welcome on GitHub at https://github.com/bolshakov/active_record-sequence. 137 | 138 | -------------------------------------------------------------------------------- /lib/active_record/sequence.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_record/sequence/version' 3 | require 'active_record/sequence/error' 4 | require 'active_support/core_ext/module/delegation' 5 | 6 | module ActiveRecord 7 | # Usage 8 | # sequence = Sequence.new('numbers') 9 | # sequence.next #=> 1 10 | # sequence.peek #=> 1 11 | # sequence.next #=> 2 12 | # 13 | class Sequence 14 | autoload :SequenceSQLBuilder, 'active_record/sequence/sequence_sql_builder' 15 | 16 | class << self 17 | CREATE_ERRORS = { 18 | 'PG::DuplicateTable' => ActiveRecord::Sequence::AlreadyExist, 19 | }.freeze 20 | # Create sequence 21 | # @param name [String] 22 | # @param options [{}] 23 | # @option options [Integer] :start (1) 24 | # @option options [Integer] :increment (1) specifies which value is added to 25 | # the current sequence value to create a new value. A positive value will 26 | # make an ascending sequence, a negative one a descending sequence. 27 | # @option options [Integer] :min determines the minimum value a sequence can generate. 28 | # The defaults are 1 and -2^63-1 for ascending and descending sequences, respectively. 29 | # @option options [Integer] :max determines the maximum value for the sequence. 30 | # The defaults are 2^63-1 and -1 for ascending and descending sequences, respectively. 31 | # @option options [Boolean] :cycle (false) allows the sequence to wrap around when the 32 | # max value or min value has been reached by an ascending or descending sequence respectively. 33 | # If the limit is reached, the next number generated will be the min value or max value, respectively. 34 | # @return [Sequence] 35 | # @see https://www.postgresql.org/docs/8.1/static/sql-createsequence.html 36 | def create(name, options = {}) 37 | create_sql = SequenceSQLBuilder.new(name, options).to_sql 38 | handle_postgres_errors(CREATE_ERRORS) do 39 | with_connection do |connection| 40 | connection.execute(create_sql) 41 | end 42 | end 43 | new(name) 44 | end 45 | 46 | DROP_ERRORS = { 47 | 'PG::UndefinedTable' => NotExist, 48 | }.freeze 49 | 50 | # @param name [String] 51 | # @return [void] 52 | def drop(name) 53 | drop_sql = format('DROP SEQUENCE %s', name) 54 | handle_postgres_errors(DROP_ERRORS) do 55 | with_connection do |connection| 56 | connection.execute(drop_sql) 57 | end 58 | end 59 | end 60 | 61 | # @api private 62 | def with_connection 63 | ActiveRecord::Base.connection_pool.with_connection do |connection| 64 | yield(connection) 65 | end 66 | end 67 | 68 | # @param mappings [{}] from PG errors to library errors 69 | # @api private 70 | def handle_postgres_errors(mappings) 71 | yield 72 | rescue ActiveRecord::StatementInvalid => error 73 | library_error = mappings.fetch(error.cause.class.name) { raise } 74 | raise library_error 75 | end 76 | end 77 | 78 | attr_reader :name 79 | 80 | # @param name [String] 81 | def initialize(name) 82 | @name = name 83 | end 84 | 85 | NEXT_ERRORS = { 86 | 'PG::ObjectNotInPrerequisiteState' => StopIteration, 87 | 'PG::SequenceGeneratorLimitExceeded' => StopIteration, 88 | 'PG::UndefinedTable' => ActiveRecord::Sequence::NotExist, 89 | }.freeze 90 | 91 | # @return [Integer] 92 | def next 93 | next_sql = 'SELECT nextval(%s)'.freeze 94 | handle_postgres_errors(NEXT_ERRORS) do 95 | execute(next_sql, name) 96 | end 97 | end 98 | 99 | PEEK_ERRORS = { 100 | 'PG::ObjectNotInPrerequisiteState' => ActiveRecord::Sequence::CurrentValueUndefined, 101 | 'PG::UndefinedTable' => ActiveRecord::Sequence::NotExist, 102 | }.freeze 103 | 104 | # @return [Integer] 105 | def peek 106 | current_sql = 'SELECT currval(%s)'.freeze 107 | handle_postgres_errors(PEEK_ERRORS) do 108 | execute(current_sql, name) 109 | end 110 | end 111 | 112 | private 113 | 114 | delegate :handle_postgres_errors, to: :class 115 | delegate :with_connection, to: :class 116 | 117 | def execute(sql, *args) 118 | with_connection do |connection| 119 | connection.select_value(prepare_query(sql, *args)).to_i 120 | end 121 | end 122 | 123 | def prepare_query(sql, *args) 124 | quoted_args = args.map do |arg| 125 | with_connection do |connection| 126 | connection.quote(arg) 127 | end 128 | end 129 | format(sql, *quoted_args) 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/sequence_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe ActiveRecord::Sequence do 2 | let(:sequence_name) { 'test_sequence' } 3 | let(:another_sequence_name) { 'another_test_sequence' } 4 | let(:sequence) { described_class.new(sequence_name) } 5 | let(:another_sequence) { described_class.new(another_sequence_name) } 6 | 7 | delegate :connection, to: ActiveRecord::Base 8 | 9 | AlreadyExist = ActiveRecord::Sequence::AlreadyExist 10 | CurrentValueUndefined = ActiveRecord::Sequence::CurrentValueUndefined 11 | NotExist = ActiveRecord::Sequence::NotExist 12 | 13 | def ensure_sequence_not_exists(name) 14 | connection.execute("DROP SEQUENCE #{name};") 15 | rescue ActiveRecord::StatementInvalid # rubocop:disable Lint/HandleExceptions 16 | end 17 | 18 | before do 19 | ensure_sequence_not_exists('test_sequence') 20 | ensure_sequence_not_exists('another_test_sequence') 21 | end 22 | 23 | describe '.create' do 24 | it 'returns created sequence' do 25 | sequence = described_class.create(sequence_name) 26 | expect(sequence.next).to eq(1) 27 | end 28 | 29 | it 'creates new sequence' do 30 | described_class.create(sequence_name) 31 | expect(sequence.next).to eq(1) 32 | end 33 | 34 | it 'creates new sequence with start value' do 35 | described_class.create(sequence_name, start: 42) 36 | expect(sequence.next).to eq(42) 37 | end 38 | 39 | it 'creates new sequence with custom positive increment' do 40 | described_class.create(sequence_name, increment: 3) 41 | expect(sequence.next).to eq(1) 42 | expect(sequence.next).to eq(4) 43 | end 44 | 45 | it 'creates new sequence with custom negative increment' do 46 | described_class.create(sequence_name, increment: -3) 47 | expect(sequence.next).to eq(-1) 48 | expect(sequence.next).to eq(-4) 49 | end 50 | 51 | it 'creates new sequence with custom minimum value' do 52 | described_class.create(sequence_name, min: -2, increment: -1) 53 | expect(sequence.next).to eq(-1) 54 | expect(sequence.next).to eq(-2) 55 | expect { sequence.next }.to raise_error(StopIteration) 56 | end 57 | 58 | it 'creates new sequence with custom maximum value' do 59 | described_class.create(sequence_name, max: 2) 60 | expect(sequence.next).to eq(1) 61 | expect(sequence.next).to eq(2) 62 | expect { sequence.next }.to raise_error(StopIteration) 63 | end 64 | 65 | it 'creates new cyclic sequence' do 66 | described_class.create(sequence_name, max: 2, cycle: true) 67 | expect(sequence.next).to eq(1) 68 | expect(sequence.next).to eq(2) 69 | expect(sequence.next).to eq(1) 70 | end 71 | 72 | it 'fails with error if sequence already exists' do 73 | described_class.create(sequence_name) 74 | expect { described_class.create(sequence_name) }.to raise_error(AlreadyExist) 75 | end 76 | end 77 | 78 | describe '.drop' do 79 | before do 80 | connection.execute("CREATE SEQUENCE #{sequence_name};") 81 | end 82 | 83 | it 'drops existing sequence' do 84 | described_class.drop(sequence_name) 85 | expect { sequence.next }.to raise_error(NotExist) 86 | end 87 | 88 | context 'when sequence not exists' do 89 | it 'fail with error' do 90 | expect { described_class.drop('another_test_sequence') }.to raise_error(NotExist) 91 | end 92 | end 93 | end 94 | 95 | describe '#next' do 96 | before do 97 | described_class.create(sequence_name) 98 | described_class.create(another_sequence_name) 99 | end 100 | 101 | it 'returns next value' do 102 | expect(sequence.next).to eq(1) 103 | expect(sequence.next).to eq(2) 104 | end 105 | 106 | it 'returns independent values for different sequences' do 107 | expect(sequence.next).to eq(1) 108 | expect(another_sequence.next).to eq(1) 109 | end 110 | 111 | context 'when called on not existing sequence' do 112 | it 'fails with NotExist error' do 113 | not_existing_sequence = described_class.new('not_existing_sequence') 114 | expect { not_existing_sequence.next }.to raise_error(NotExist) 115 | end 116 | end 117 | end 118 | 119 | describe '#peek' do 120 | before do 121 | described_class.create(sequence_name) 122 | described_class.create(another_sequence_name) 123 | end 124 | 125 | context 'when called on sequence with not defined current value' do 126 | it 'fails with error' do 127 | expect { sequence.peek }.to raise_error(CurrentValueUndefined) 128 | end 129 | end 130 | 131 | context 'when called on sequence with defined current value' do 132 | it 'returns current value' do 133 | sequence.next 134 | expect(sequence.peek).to eq(1) 135 | end 136 | 137 | it 'returns independent values for different sequences' do 138 | sequence.next 139 | another_sequence.next 140 | another_sequence.next 141 | 142 | expect(sequence.peek).to eq(1) 143 | expect(another_sequence.peek).to eq(2) 144 | end 145 | end 146 | 147 | context 'when called on not existing sequence' do 148 | subject { -> { not_existing_sequence.peek } } 149 | 150 | let(:not_existing_sequence) { described_class.new('not_existing_sequence') } 151 | 152 | it { is_expected.to raise_error(NotExist) } 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 2 | require 'active_record/sequence' 3 | 4 | ActiveRecord::Base.establish_connection( 5 | adapter: 'postgresql', 6 | database: 'travis_ci_test', 7 | ) 8 | 9 | # This file was generated by the `rspec --init` command. Conventionally, all 10 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 11 | # The generated `.rspec` file contains `--require spec_helper` which will cause 12 | # this file to always be loaded, without a need to explicitly require it in any 13 | # files. 14 | # 15 | # Given that it is always loaded, you are encouraged to keep this file as 16 | # light-weight as possible. Requiring heavyweight dependencies from this file 17 | # will add to the boot time of your test suite on EVERY test run, even for an 18 | # individual file that may not need all of that loaded. Instead, consider making 19 | # a separate helper file that requires the additional dependencies and performs 20 | # the additional setup, and require it from the spec files that actually need 21 | # it. 22 | # 23 | # The `.rspec` file also contains a few flags that are not defaults but that 24 | # users commonly want. 25 | # 26 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 27 | RSpec.configure do |config| 28 | # rspec-expectations config goes here. You can use an alternate 29 | # assertion/expectation library such as wrong or the stdlib/minitest 30 | # assertions if you prefer. 31 | config.expect_with :rspec do |expectations| 32 | # This option will default to `true` in RSpec 4. It makes the `description` 33 | # and `failure_message` of custom matchers include text for helper methods 34 | # defined using `chain`, e.g.: 35 | # be_bigger_than(2).and_smaller_than(4).description 36 | # # => "be bigger than 2 and smaller than 4" 37 | # ...rather than: 38 | # # => "be bigger than 2" 39 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 40 | end 41 | 42 | # rspec-mocks config goes here. You can use an alternate test double 43 | # library (such as bogus or mocha) by changing the `mock_with` option here. 44 | config.mock_with :rspec do |mocks| 45 | # Prevents you from mocking or stubbing a method that does not exist on 46 | # a real object. This is generally recommended, and will default to 47 | # `true` in RSpec 4. 48 | mocks.verify_partial_doubles = true 49 | end 50 | 51 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 52 | # have no way to turn it off -- the option exists only for backwards 53 | # compatibility in RSpec 3). It causes shared context metadata to be 54 | # inherited by the metadata hash of host groups and examples, rather than 55 | # triggering implicit auto-inclusion in groups with matching metadata. 56 | config.shared_context_metadata_behavior = :apply_to_host_groups 57 | 58 | # The settings below are suggested to provide a good initial experience 59 | # with RSpec, but feel free to customize to your heart's content. 60 | # # This allows you to limit a spec run to individual examples or groups 61 | # # you care about by tagging them with `:focus` metadata. When nothing 62 | # # is tagged with `:focus`, all examples get run. RSpec also provides 63 | # # aliases for `it`, `describe`, and `context` that include `:focus` 64 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 65 | # config.filter_run_when_matching :focus 66 | # 67 | # # Allows RSpec to persist some state between runs in order to support 68 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 69 | # # you configure your source control system to ignore this file. 70 | # config.example_status_persistence_file_path = "spec/examples.txt" 71 | # 72 | # # Limits the available syntax to the non-monkey patched syntax that is 73 | # # recommended. For more details, see: 74 | # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 75 | # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 76 | # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 77 | # config.disable_monkey_patching! 78 | # 79 | # # This setting enables warnings. It's recommended, but in some cases may 80 | # # be too noisy due to issues in dependencies. 81 | # config.warnings = true 82 | # 83 | # # Many RSpec users commonly either run the entire suite or an individual 84 | # # file, and it's useful to allow more verbose output when running an 85 | # # individual spec file. 86 | # if config.files_to_run.one? 87 | # # Use the documentation formatter for detailed output, 88 | # # unless a formatter has already been configured 89 | # # (e.g. via a command-line flag). 90 | # config.default_formatter = 'doc' 91 | # end 92 | # 93 | # # Print the 10 slowest examples and example groups at the 94 | # # end of the spec run, to help surface which specs are running 95 | # # particularly slow. 96 | # config.profile_examples = 10 97 | # 98 | # # Run specs in random order to surface order dependencies. If you find an 99 | # # order dependency and want to debug it, you can fix the order by providing 100 | # # the seed, which is printed after each run. 101 | # # --seed 1234 102 | # config.order = :random 103 | # 104 | # # Seed global randomization in this process using the `--seed` CLI option. 105 | # # Setting this allows you to use `--seed` to deterministically reproduce 106 | # # test failures related to randomization by passing the same `--seed` value 107 | # # as the one that triggered the failure. 108 | # Kernel.srand config.seed 109 | end 110 | --------------------------------------------------------------------------------