pg_random_id 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pg_random_id.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Rafał Rzepecki
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.
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # PgRandomId
2
+
3
+ Allow usage of pseudo-random IDs in Postgresql databases.
4
+ Changes sequential surrogate ids (1, 2, 3...) into a pseudo-random
5
+ sequence of unique 30-bits nonnegative integer values (eg. 760280231, 110168588, 1029278017...).
6
+
7
+ Integrates with ActiveRecord. Sequel integration is upcoming.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'pg_random_id'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install pg_random_id
22
+
23
+ ## Usage
24
+
25
+ The easiest way to use it is to use the supplied migration functions.
26
+
27
+ First, make sure you put
28
+ create_random_id_functions
29
+ in a migration (a single one will do).
30
+
31
+ Then to apply random ids to a table, use random_id function:
32
+
33
+ ```ruby
34
+ class RandomizeIdsOnFoo < ActiveRecord::Migration
35
+ def up
36
+ KEY = 21315
37
+ random_id :foo, :foo_id, KEY
38
+ end
39
+ end
40
+ ```
41
+
42
+ If you don't supply the key, a random one will be generated;
43
+ similarly, the id column name defaults to :id.
44
+ This means that you can create a vanilla AR table with random ids
45
+ with the following simple migration:
46
+
47
+ ```ruby
48
+ class CreateProducts < ActiveRecord::Migration
49
+ def up
50
+ create_table :products do |t|
51
+ t.string :name
52
+ end
53
+ random_id :products
54
+ end
55
+ end
56
+ ```
57
+
58
+ No model modification is necessary, just use the table as usual and it will simply work.
59
+ You can even use it without ActiveRecord.
60
+
61
+ ### Text ids
62
+
63
+ If you use random_str_id function instead, it will additionally
64
+ change the column type to character(6) and store the ids as base32-encoded
65
+ strings of handy human-friendly form (eg. kn5xx1, qy2kp8, e5f67z...).
66
+
67
+ ## Contributing
68
+
69
+ 1. Fork it
70
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
71
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
72
+ 4. Push to the branch (`git push origin my-new-feature`)
73
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,35 @@
1
+ require 'active_record'
2
+ require 'active_record/migration'
3
+ require 'pg_random_id/sql'
4
+
5
+ module PgRandomId
6
+ module Migrations
7
+ module ActiveRecord
8
+ # Create in the database the function (pri_scramble(bigint, bigint))
9
+ # necessary for this gem to work.
10
+ def create_random_id_functions
11
+ execute PgRandomId::Sql::install
12
+ end
13
+
14
+ # Apply a random id to a table.
15
+ # If you don't give a key, a random one will be generated.
16
+ # The ids will be based on sequence "#{table}_#{column}_seq".
17
+ # You need to make sure the table is empty; migrating existing records is not implemented.
18
+ def random_id table, column = :id, key = nil
19
+ execute PgRandomId::Sql::apply(table, column, key)
20
+ end
21
+
22
+ # Apply a random string id to a table.
23
+ # Also changes the type of the id column to char(6).
24
+ # If you don't give a key, a random one will be generated.
25
+ # The ids will be based on sequence "#{table}_#{column}_seq",
26
+ # scrambled and base32-encoded.
27
+ # You need to make sure the table is empty; migrating existing records is not implemented.
28
+ def random_str_id table, column = :id, key = nil
29
+ execute PgRandomId::Sql::apply_str(table, column, key)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ ActiveRecord::Migration.send :include, PgRandomId::Migrations::ActiveRecord
@@ -0,0 +1,15 @@
1
+ -- Pure Postgres SQL implementation of Base32 Crockford encoding
2
+ -- using recursive common table expressions
3
+ -- (about 10% slower than the pg/plsql variant)
4
+ CREATE OR REPLACE FUNCTION crockford(input bigint) RETURNS varchar
5
+ LANGUAGE sql
6
+ IMMUTABLE STRICT AS $$
7
+ WITH RECURSIVE parts(part) AS (
8
+ (SELECT $1 AS part)
9
+ UNION
10
+ (SELECT part >> 5 FROM parts WHERE part > 31))
11
+ SELECT string_agg(
12
+ (ARRAY['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'])
13
+ [(part & 31) + 1]
14
+ ,'') FROM (SELECT part FROM parts ORDER BY part ASC) AS reversed;
15
+ $$;
@@ -0,0 +1,23 @@
1
+ -- Postgres pl/pgSQL implementation of Base32 Crockford encoding
2
+ -- (see http://www.crockford.com/wrmg/base32.html )
3
+
4
+ CREATE OR REPLACE FUNCTION crockford(input bigint) RETURNS varchar
5
+ LANGUAGE plpgsql
6
+ IMMUTABLE STRICT AS $$
7
+ DECLARE
8
+ charmap CONSTANT char[] = ARRAY['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'];
9
+ result varchar = '';
10
+ BEGIN
11
+ IF (input < 0) THEN
12
+ RAISE EXCEPTION numeric_value_out_of_range USING
13
+ MESSAGE = 'crockford(): bad input value --> ' || input,
14
+ HINT = 'Input needs to be nonnegative.';
15
+ END IF;
16
+ LOOP
17
+ result := charmap[(input & 31) + 1] || result;
18
+ input := input >> 5;
19
+ EXIT WHEN input = 0;
20
+ END LOOP;
21
+ RETURN result;
22
+ END;
23
+ $$;
@@ -0,0 +1,38 @@
1
+ -- A simple Feistel network to scramble a 30-bit unsigned integer.
2
+ -- There are seven rounds with a simple round function.
3
+ -- The output is guaranteed to be 1:1 mapping, ie. no collisions.
4
+ -- Supply a unique key (table oid is a good choice) to create different sequences.
5
+ -- 30-bit length makes it possible to use ints instead of bigints
6
+ -- and convenient to base32-encode the value.
7
+
8
+ -- The code is based on http://wiki.postgresql.org/wiki/Pseudo_encrypt
9
+ CREATE OR REPLACE FUNCTION
10
+ pri_scramble(key bigint, input bigint) RETURNS int
11
+ LANGUAGE plpgsql
12
+ IMMUTABLE STRICT
13
+ AS $$
14
+ DECLARE
15
+ l1 int;
16
+ l2 int;
17
+ r1 int;
18
+ r2 int;
19
+ i int:=0;
20
+ BEGIN
21
+ IF (input < 0) OR (input > 1073741823) THEN
22
+ RAISE EXCEPTION numeric_value_out_of_range USING
23
+ MESSAGE = 'pri_scramble(): bad input value --> ' || input,
24
+ HINT = 'Input needs to be a 30-bit nonnegative integer.';
25
+ END IF;
26
+ l1:= (input >> 15) & 32767;
27
+ r1:= input & 32767;
28
+ key := key + 15228; -- we don't want the key to be zero
29
+ WHILE i < 7 LOOP
30
+ l2 := r1;
31
+ r2 := l1 # ((r1::bigint * key + 3506) & 32767);
32
+ l1 := l2;
33
+ r1 := r2;
34
+ i := i + 1;
35
+ END LOOP;
36
+ RETURN ((l1 << 15) + r1);
37
+ END;
38
+ $$;
@@ -0,0 +1,37 @@
1
+ module PgRandomId
2
+ module Sql
3
+ class << self
4
+ def install
5
+ FILES.map {|f| read_file f}.join
6
+ end
7
+
8
+ def apply table, column, key = nil, base = nil
9
+ key ||= rand(2**15)
10
+ base ||= sequence_nextval "#{table}_#{column}_seq"
11
+ "ALTER TABLE #{table} ALTER COLUMN #{column} SET DEFAULT pri_scramble(#{key}, #{base})"
12
+ end
13
+
14
+ def apply_str table, column, key = nil, base = nil
15
+ key ||= rand(2**15)
16
+ base ||= sequence_nextval "#{table}_#{column}_seq"
17
+ """
18
+ ALTER TABLE #{table} ALTER COLUMN #{column} SET DATA TYPE character(6);
19
+ ALTER TABLE #{table} ALTER COLUMN #{column} SET DEFAULT lpad(crockford(pri_scramble(#{key}, #{base})), 6, '0');
20
+ """
21
+ end
22
+
23
+ private
24
+
25
+ FILES = %w(scramble.sql crockford.sql)
26
+ BASEDIR = File.expand_path 'sql', File.dirname(__FILE__)
27
+
28
+ def read_file filename
29
+ File.read(File.expand_path(filename, BASEDIR))
30
+ end
31
+
32
+ def sequence_nextval sequence_name
33
+ "nextval('#{sequence_name}'::regclass)"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module PgRandomId
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "pg_random_id/sql"
2
+ require "pg_random_id/version"
3
+
4
+ if defined? ActiveRecord
5
+ require "pg_random_id/migrations/active_record"
6
+ end
7
+
8
+ module PgRandomId
9
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pg_random_id/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pg_random_id"
8
+ gem.version = PgRandomId::VERSION
9
+ gem.authors = ["Rafał Rzepecki"]
10
+ gem.email = ["divided.mind@gmail.com"]
11
+ gem.description = %q{Easily use randomized integers instead of sequential values for your record surrogate ids.}
12
+ gem.summary = %q{Pseudo-random record ids in Postgres}
13
+ gem.homepage = "https://github.com/dividedmind/pg_random_id"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ %w(activerecord rspec).each do |g|
21
+ gem.add_development_dependency g
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ shared_context 'active_record' do
2
+ before do
3
+ dburl = ENV['TEST_DATABASE_URL'] || 'postgres:///pg_random_id_test'
4
+ ActiveRecord::Base.establish_connection dburl
5
+ ActiveRecord::Base.connection.execute 'BEGIN;'
6
+ end
7
+
8
+ after do
9
+ ActiveRecord::Base.connection.execute 'ROLLBACK;'
10
+ end
11
+
12
+ let(:migration) {
13
+ Class.new(ActiveRecord::Migration)
14
+ }
15
+
16
+ def execute code
17
+ ActiveRecord::Base.connection.select_one(code)
18
+ end
19
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ require 'active_record'
4
+ require 'pg_random_id/migrations/active_record'
5
+
6
+ describe PgRandomId::Migrations::ActiveRecord do
7
+ include_context 'active_record'
8
+ describe '#create_random_id_functions' do
9
+ it "installs the pri_scramble function" do
10
+ migration.create_random_id_functions
11
+
12
+ execute("SELECT 1 FROM pg_proc WHERE proname = 'crockford'").should be
13
+ execute("SELECT 1 FROM pg_proc WHERE proname = 'pri_scramble'").should be
14
+ end
15
+ end
16
+
17
+ describe '#random_id' do
18
+ it "changes the default value" do
19
+ migration.create_random_id_functions
20
+ migration.create_table :foo
21
+ migration.random_id :foo
22
+ execute("SELECT 1 FROM pg_attrdef WHERE adrelid = 'foo'::regclass AND adsrc LIKE '%pri_scramble%'").should be
23
+ end
24
+
25
+ it "creates a few values without error" do
26
+ migration.create_random_id_functions
27
+ migration.create_table :foo
28
+ migration.random_id :foo
29
+ 10.times do
30
+ expect {
31
+ ActiveRecord::Base.connection.execute('INSERT INTO foo VALUES(default)')
32
+ }.to_not raise_error
33
+ end
34
+ execute("SELECT COUNT(*) FROM foo")['count'].to_i.should == 10
35
+ end
36
+ end
37
+
38
+ describe '#random_str_id' do
39
+ it "changes the default value" do
40
+ migration.create_random_id_functions
41
+ migration.create_table :foo
42
+ migration.random_str_id :foo
43
+ execute("SELECT 1 FROM pg_attrdef WHERE adrelid = 'foo'::regclass AND adsrc LIKE '%pri_scramble%'").should be
44
+ execute("SELECT 1 FROM pg_attrdef WHERE adrelid = 'foo'::regclass AND adsrc LIKE '%crockford%'").should be
45
+ execute("INSERT INTO foo VALUES (DEFAULT) RETURNING id;")['id'].should_not == '1'
46
+ end
47
+
48
+ it "changes the type" do
49
+ migration.create_random_id_functions
50
+ migration.create_table :foo
51
+ migration.random_str_id :foo
52
+ execute("""
53
+ SELECT typname FROM pg_attribute, pg_type
54
+ WHERE attrelid = 'foo'::regclass
55
+ AND attname = 'id'
56
+ AND atttypid = typelem
57
+ AND typname LIKE '%int%'
58
+ """).should_not be
59
+ end
60
+
61
+ it "creates a few values without error" do
62
+ migration.create_random_id_functions
63
+ migration.create_table :foo
64
+ migration.random_str_id :foo
65
+ 10.times do
66
+ expect {
67
+ ActiveRecord::Base.connection.execute('INSERT INTO foo VALUES(default)')
68
+ }.to_not raise_error
69
+ end
70
+ execute("SELECT COUNT(*) FROM foo")['count'].to_i.should == 10
71
+ end
72
+ end
73
+ end
@@ -0,0 +1 @@
1
+ require 'helpers/active_record_helper'
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ require 'active_record'
4
+ require 'pg_random_id/migrations/active_record'
5
+
6
+ describe 'crockford(bigint)' do
7
+ include_context 'active_record'
8
+ before do
9
+ migration.create_random_id_functions
10
+ end
11
+
12
+ def crockford argument
13
+ execute("SELECT crockford(#{argument})")['crockford']
14
+ end
15
+
16
+ it "returns correct encodings for a few chosen values" do
17
+ {
18
+ 0 => '0',
19
+ 1 => '1',
20
+ 31 => 'z',
21
+ 32 => '10',
22
+ 1712969022158652754 => '1fhdg7zyg7haj',
23
+ 9223372036854775807 => '7zzzzzzzzzzzz'
24
+ }.each {|k, v| crockford(k).should == v}
25
+ end
26
+
27
+ it "errors out on negative numbers" do
28
+ expect{crockford(-1)}.to raise_error
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ require 'active_record'
4
+ require 'pg_random_id/migrations/active_record'
5
+
6
+ describe "pri_scramble(bigint, bigint)" do
7
+ include_context 'active_record'
8
+ before do
9
+ migration.create_random_id_functions
10
+ end
11
+
12
+ it "seems to have no collisions" do
13
+ COUNT = 10000
14
+ KEY = rand(2**32)
15
+ CODE = "SELECT COUNT(DISTINCT pri_scramble(#{KEY}, seq)) FROM generate_series(1, #{COUNT}) AS s(seq)"
16
+ execute(CODE)['count'].to_i.should == COUNT
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_random_id
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rafał Rzepecki
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Easily use randomized integers instead of sequential values for your
47
+ record surrogate ids.
48
+ email:
49
+ - divided.mind@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - lib/pg_random_id.rb
60
+ - lib/pg_random_id/migrations/active_record.rb
61
+ - lib/pg_random_id/sql.rb
62
+ - lib/pg_random_id/sql/crockford-pure.sql
63
+ - lib/pg_random_id/sql/crockford.sql
64
+ - lib/pg_random_id/sql/scramble.sql
65
+ - lib/pg_random_id/version.rb
66
+ - pg_random_id.gemspec
67
+ - spec/helpers/active_record_helper.rb
68
+ - spec/migrations/active_record_spec.rb
69
+ - spec/spec_helper.rb
70
+ - spec/sql/crockford_spec.rb
71
+ - spec/sql/scramble_spec.rb
72
+ homepage: https://github.com/dividedmind/pg_random_id
73
+ licenses: []
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.24
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Pseudo-random record ids in Postgres
96
+ test_files:
97
+ - spec/helpers/active_record_helper.rb
98
+ - spec/migrations/active_record_spec.rb
99
+ - spec/spec_helper.rb
100
+ - spec/sql/crockford_spec.rb
101
+ - spec/sql/scramble_spec.rb
102
+ has_rdoc: