low_card_tables 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +59 -0
- data/Gemfile +17 -0
- data/LICENSE +21 -0
- data/README.md +75 -0
- data/Rakefile +6 -0
- data/lib/low_card_tables.rb +72 -0
- data/lib/low_card_tables/active_record/base.rb +55 -0
- data/lib/low_card_tables/active_record/migrations.rb +223 -0
- data/lib/low_card_tables/active_record/relation.rb +35 -0
- data/lib/low_card_tables/active_record/scoping.rb +87 -0
- data/lib/low_card_tables/errors.rb +74 -0
- data/lib/low_card_tables/has_low_card_table/base.rb +114 -0
- data/lib/low_card_tables/has_low_card_table/low_card_association.rb +273 -0
- data/lib/low_card_tables/has_low_card_table/low_card_associations_manager.rb +143 -0
- data/lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb +224 -0
- data/lib/low_card_tables/has_low_card_table/low_card_objects_manager.rb +80 -0
- data/lib/low_card_tables/low_card_table/base.rb +184 -0
- data/lib/low_card_tables/low_card_table/cache.rb +214 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/exponential_cache_expiration_policy.rb +151 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/fixed_cache_expiration_policy.rb +23 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/has_cache_expiration.rb +100 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/no_caching_expiration_policy.rb +13 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/unlimited_cache_expiration_policy.rb +13 -0
- data/lib/low_card_tables/low_card_table/row_collapser.rb +175 -0
- data/lib/low_card_tables/low_card_table/row_manager.rb +681 -0
- data/lib/low_card_tables/low_card_table/table_unique_index.rb +134 -0
- data/lib/low_card_tables/version.rb +4 -0
- data/lib/low_card_tables/version_support.rb +52 -0
- data/low_card_tables.gemspec +69 -0
- data/spec/low_card_tables/helpers/database_helper.rb +148 -0
- data/spec/low_card_tables/helpers/query_spy_helper.rb +47 -0
- data/spec/low_card_tables/helpers/system_helpers.rb +63 -0
- data/spec/low_card_tables/system/basic_system_spec.rb +254 -0
- data/spec/low_card_tables/system/bulk_system_spec.rb +334 -0
- data/spec/low_card_tables/system/caching_system_spec.rb +531 -0
- data/spec/low_card_tables/system/migrations_system_spec.rb +747 -0
- data/spec/low_card_tables/system/options_system_spec.rb +581 -0
- data/spec/low_card_tables/system/queries_system_spec.rb +142 -0
- data/spec/low_card_tables/system/validations_system_spec.rb +88 -0
- data/spec/low_card_tables/unit/active_record/base_spec.rb +53 -0
- data/spec/low_card_tables/unit/active_record/migrations_spec.rb +207 -0
- data/spec/low_card_tables/unit/active_record/relation_spec.rb +47 -0
- data/spec/low_card_tables/unit/active_record/scoping_spec.rb +101 -0
- data/spec/low_card_tables/unit/has_low_card_table/base_spec.rb +79 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_association_spec.rb +287 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_associations_manager_spec.rb +190 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_dynamic_method_manager_spec.rb +234 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_objects_manager_spec.rb +70 -0
- data/spec/low_card_tables/unit/low_card_table/base_spec.rb +207 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/exponential_cache_expiration_policy_spec.rb +128 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/fixed_cache_expiration_policy_spec.rb +25 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/has_cache_expiration_policy_spec.rb +100 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/no_caching_expiration_policy_spec.rb +14 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/unlimited_cache_expiration_policy_spec.rb +14 -0
- data/spec/low_card_tables/unit/low_card_table/cache_spec.rb +282 -0
- data/spec/low_card_tables/unit/low_card_table/row_collapser_spec.rb +109 -0
- data/spec/low_card_tables/unit/low_card_table/row_manager_spec.rb +918 -0
- data/spec/low_card_tables/unit/low_card_table/table_unique_index_spec.rb +117 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d7c2ebbfc8004ec52dcffbae127d9b17dadf62cb
|
4
|
+
data.tar.gz: f8b2f393e3a18649a01b8d53baefd0101e879c17
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d37e5bddb60b90c475b6a33a42979f15ebc88fc78f6581856b6d2f971370c175d7fdc10000981027c5660f3fd709e1075160f88f2255106a9dd577b16454abc7
|
7
|
+
data.tar.gz: 312b865016ead5b60440bed7a9cb80b7aed32c110eb87847062570e7f86ba59e8f3f688270b39a0e675af8d9603cd650e34fd0adc5b27d4414285692b65d6da7
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
rvm:
|
2
|
+
- "1.8.7"
|
3
|
+
- "1.9.3"
|
4
|
+
- "2.0.0"
|
5
|
+
- "jruby-1.7.6"
|
6
|
+
env:
|
7
|
+
# Sadly, Travis seems to have a version of SQLite < 3.7.11 installed on many of its workers;
|
8
|
+
# this prevents activerecord-import from working, since those versions of the SQLite engine
|
9
|
+
# don't have support for multi-row inserts in a single statement. There really isn't anything
|
10
|
+
# we can do about this, unfortunately..
|
11
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=3.0.20 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=mysql
|
12
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=3.0.20 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
13
|
+
# - LOW_CARD_TABLES_AR_TEST_VERSION=3.0.20 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
14
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=mysql
|
15
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
16
|
+
# - LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
17
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=3.2.15 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=mysql
|
18
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=3.2.15 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
19
|
+
# - LOW_CARD_TABLES_AR_TEST_VERSION=3.2.15 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
20
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=mysql
|
21
|
+
- LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
22
|
+
# - LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
23
|
+
before_script:
|
24
|
+
- export JRUBY_OPTS="-J-Xmx256m -J-Xms256m $JRUBY_OPTS"
|
25
|
+
- mysql -e 'create database myapp_test;'
|
26
|
+
- psql -c 'create database myapp_test;' -U postgres
|
27
|
+
matrix:
|
28
|
+
exclude:
|
29
|
+
# ActiveRecord 4.x doesn't support Ruby 1.8.7
|
30
|
+
- rvm: 1.8.7
|
31
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=mysql
|
32
|
+
- rvm: 1.8.7
|
33
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
34
|
+
- rvm: 1.8.7
|
35
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
36
|
+
# There's a bug in ActiveRecord 3.1.x that makes it incompatible with Ruby 2.0
|
37
|
+
- rvm: 2.0.0
|
38
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=mysql
|
39
|
+
- rvm: 2.0.0
|
40
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
41
|
+
- rvm: 2.0.0
|
42
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
43
|
+
# The activerecord-import gem currently doesn't support JRuby JDBC adapters with anything but MySQL
|
44
|
+
- rvm: jruby-1.7.6
|
45
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.0.20 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
46
|
+
- rvm: jruby-1.7.6
|
47
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
48
|
+
- rvm: jruby-1.7.6
|
49
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.2.15 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
50
|
+
- rvm: jruby-1.7.6
|
51
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=postgres
|
52
|
+
- rvm: jruby-1.7.6
|
53
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.0.20 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
54
|
+
- rvm: jruby-1.7.6
|
55
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.1.12 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
56
|
+
- rvm: jruby-1.7.6
|
57
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=3.2.15 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
58
|
+
- rvm: jruby-1.7.6
|
59
|
+
env: LOW_CARD_TABLES_AR_TEST_VERSION=4.0.1 LOW_CARD_TABLES_TRAVIS_CI_DATABASE_TYPE=sqlite
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in low_card_tables.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
ar_version = ENV['LOW_CARD_TABLES_AR_TEST_VERSION']
|
7
|
+
ar_version = ar_version.strip if ar_version
|
8
|
+
|
9
|
+
version_spec = case ar_version
|
10
|
+
when nil then nil
|
11
|
+
when 'master' then { :git => 'git://github.com/rails/activerecord.git' }
|
12
|
+
else "=#{ar_version}"
|
13
|
+
end
|
14
|
+
|
15
|
+
if version_spec
|
16
|
+
gem("activerecord", version_spec)
|
17
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2013, Andrew Geweke
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# low_card_tables
|
2
|
+
|
3
|
+
Think of `low_card_tables` as "bitfields for ActiveRecord, but done right". It allows you to store multiple values
|
4
|
+
as compactly as possible in a given database column, but using a technique that's vastly friendlier to queries,
|
5
|
+
future expansion, separate analysis, and other (non-Rails) tools than actual bitfields. It works with any data that
|
6
|
+
has few distinct values in the table; boolean fields are one example, but any `enum`-style fields are great candidates
|
7
|
+
for use.
|
8
|
+
|
9
|
+
Greatly improve scalability and maintainability of your database tables by breaking out columns containing few distinct values (e.g., booleans and other flags) into a separate table that's transparently referenced and used. Supports Rails 3.0.x, 3.1.x, 3.2.x, and 4.0.x, running on Ruby 1.8.7, 1.9.3, and 2.0.0 with MySQL, PostgreSQL, and Sqlite. (JRuby is supported, but only with MySQL, because `low_card_tables` depends on the `activerecord-import` gem, and it currently does not have JRuby support for anything but MySQL.) Adding support for other databases is trivial.
|
10
|
+
|
11
|
+
`low_card_tables` is the successor to similar, but more primitive, systems that have been in place at very large commercial websites serving tens of millions of pages a day, and in database tables with hundreds of millions of rows. The predecessor systems were extremely successful and reliable — hence the desire to evolve this into an open-source gem.
|
12
|
+
|
13
|
+
`low_card_tables` is short for "low-cardinality tables". Cardinality, when applied to a database column, is the measure of the number of distinct values that column can hold. This Gem is meant to be used for columns that hold few distinct values throughout the table — hence, they have low cardinality.
|
14
|
+
|
15
|
+
Current build status: ![Current Build Status](https://api.travis-ci.org/ageweke/low_card_tables.png?branch=master)
|
16
|
+
|
17
|
+
===
|
18
|
+
# Documentation is on [the Wiki](https://github.com/ageweke/low_card_tables/wiki)!
|
19
|
+
|
20
|
+
This file would be incredibly long if it contained all the information present there. A quickstart guide is below;
|
21
|
+
see the Wiki for everything else.
|
22
|
+
|
23
|
+
===
|
24
|
+
|
25
|
+
### Installing low_card_tables
|
26
|
+
|
27
|
+
# Gemfile
|
28
|
+
gem 'low_card_tables'
|
29
|
+
|
30
|
+
### Getting Started
|
31
|
+
|
32
|
+
We'll first discuss adding entirely new tables, and then talk about how you can migrate existing tables.
|
33
|
+
|
34
|
+
#### Creating the Database Structure
|
35
|
+
|
36
|
+
Create the table structure you need in your database:
|
37
|
+
|
38
|
+
class MyMigration < ActiveRecord::Migration
|
39
|
+
def up
|
40
|
+
create_table :users do |t|
|
41
|
+
t.string :first_name, :null => false
|
42
|
+
t.string :last_name, :null => false
|
43
|
+
...
|
44
|
+
t.integer :user_status_id, :null => false, :limit => 2
|
45
|
+
...
|
46
|
+
end
|
47
|
+
|
48
|
+
create_table :user_statuses, :low_card => true do |t|
|
49
|
+
t.boolean :deleted, :null => false
|
50
|
+
t.boolean :deceased, :null => false
|
51
|
+
t.string :gender, :null => false, :limit => 20
|
52
|
+
t.string :payment_status, :null => false, :limit => 30
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
In the migration, we simply create the table structure in the most straightforward way possible, with one exception: we add `:low_card => true` to the `create_table` command on the low-card table itself. The only thing this does is that, once the table has been created, it automatically adds a unique index across all columns in the table — this is very important, since it allows the database to enforce the key property of the low-card system: that there is exactly one row for each unique combination of values in the low-card columns.
|
58
|
+
|
59
|
+
#### Creating the Models
|
60
|
+
|
61
|
+
Create the models:
|
62
|
+
|
63
|
+
# app/models/user_status.rb
|
64
|
+
class UserStatus < ActiveRecord::Base
|
65
|
+
is_low_card_table
|
66
|
+
end
|
67
|
+
|
68
|
+
# app/models/user.rb
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
has_low_card_table :status
|
71
|
+
end
|
72
|
+
|
73
|
+
And boom, you're done. Any columns present on `user_statuses` will appear as virtual columns on `User` — for reading and writing, for queries, for scopes, for validations, and so on.
|
74
|
+
|
75
|
+
Please see [the Wiki](https://github.com/ageweke/low_card_tables/wiki) for further documentation!
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_record/migration'
|
4
|
+
require "low_card_tables/version"
|
5
|
+
require "low_card_tables/version_support"
|
6
|
+
require 'low_card_tables/active_record/base'
|
7
|
+
require 'low_card_tables/active_record/migrations'
|
8
|
+
require 'low_card_tables/active_record/relation'
|
9
|
+
require 'low_card_tables/active_record/scoping'
|
10
|
+
require 'low_card_tables/low_card_table/cache_expiration/has_cache_expiration'
|
11
|
+
|
12
|
+
# This is the root 'require' file for +low_card_tables+. It loads a number of dependencies, and sets up some very
|
13
|
+
# basic infrastructure.
|
14
|
+
|
15
|
+
# The only thing that's actually present on the root LowCardTables module is cache-expiration settings -- you can say
|
16
|
+
# <tt>LowCardTables.low_card_cache_expiration ...</tt> to set the cache expiration for any table that has not explicitly
|
17
|
+
# had its own cache expiration defined.
|
18
|
+
module LowCardTables
|
19
|
+
include LowCardTables::LowCardTable::CacheExpiration::HasCacheExpiration
|
20
|
+
|
21
|
+
# By default, we use the ExponentialCacheExpirationPolicy with default settings.
|
22
|
+
low_card_cache_expiration :exponential
|
23
|
+
end
|
24
|
+
|
25
|
+
# Include into ActiveRecord::Base two modules -- one allows you to declare +is_low_card_table+ or +has_low_card_table+,
|
26
|
+
# and the other makes sure that you don't define scopes statically. (See LowCardTables::ActiveRecord::Scoping for more
|
27
|
+
# information on why this is really bad.)
|
28
|
+
class ActiveRecord::Base
|
29
|
+
include LowCardTables::ActiveRecord::Base
|
30
|
+
include LowCardTables::ActiveRecord::Scoping
|
31
|
+
end
|
32
|
+
|
33
|
+
# ActiveRecord migration methods (e.g., #create_table, #remove_column, etc.) are actually defined on the connection
|
34
|
+
# classes used by ActiveRecord. Here, we make sure that we get a chance to patch any connection used in any migration
|
35
|
+
# properly, so that we can add our migration support to it. See LowCardTables::ActiveRecord::Migrations for more
|
36
|
+
# information.
|
37
|
+
class ActiveRecord::Migration
|
38
|
+
if LowCardTables::VersionSupport.migrate_is_a_class_method?
|
39
|
+
class << self
|
40
|
+
def migrate_with_low_card_connection_patching(*args, &block)
|
41
|
+
_low_card_patch_connection_class_if_necessary(connection.class)
|
42
|
+
migrate_without_low_card_connection_patching(*args, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method_chain :migrate, :low_card_connection_patching
|
46
|
+
end
|
47
|
+
else
|
48
|
+
def migrate_with_low_card_connection_patching(*args, &block)
|
49
|
+
self.class._low_card_patch_connection_class_if_necessary(connection.class)
|
50
|
+
migrate_without_low_card_connection_patching(*args, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method_chain :migrate, :low_card_connection_patching
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
def _low_card_patch_connection_class_if_necessary(connection_class)
|
58
|
+
@_low_card_patched_connection_classes = { }
|
59
|
+
@_low_card_patched_connection_classes[connection_class] ||= begin
|
60
|
+
connection_class.send(:include, ::LowCardTables::ActiveRecord::Migrations)
|
61
|
+
true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# This patches in our support for queries (like #where) to all ActiveRecord::Relation objects, so that you can say
|
68
|
+
# things like <tt>User.where(:deleted => false)</tt> and it'll do exactly the right thing, automatically, even if
|
69
|
+
# +:deleted+ is actually an attribute on a low-card table associated with +User+.
|
70
|
+
class ActiveRecord::Relation
|
71
|
+
include LowCardTables::ActiveRecord::Relation
|
72
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'low_card_tables/low_card_table/base'
|
4
|
+
require 'low_card_tables/has_low_card_table/base'
|
5
|
+
|
6
|
+
module LowCardTables
|
7
|
+
module ActiveRecord
|
8
|
+
# This is a module that gets included into ActiveRecord::Base. It provides just the bootstrap
|
9
|
+
# for LowCardTables methods that let you declare +is_low_card_table+ or +has_low_card_table+,
|
10
|
+
# and see if it's a low-card table or not.
|
11
|
+
module Base
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
# Declares that this is a low-card table. This simply includes the LowCardTables::LowCardTable::Base
|
16
|
+
# module, and then calls that module's is_low_card_table method, which is the one that does all
|
17
|
+
# the real work.
|
18
|
+
def is_low_card_table(options = { })
|
19
|
+
unless @_low_card_is_low_card_table_included
|
20
|
+
include LowCardTables::LowCardTable::Base
|
21
|
+
@_low_card_is_low_card_table_included = true
|
22
|
+
end
|
23
|
+
|
24
|
+
is_low_card_table(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Is this a low-card table? This implementation just returns false -- if this is a low-card table,
|
28
|
+
# then it will have had the LowCardTables::LowCardTable::Base module included in after this one, and
|
29
|
+
# that implementation will return true.
|
30
|
+
def is_low_card_table?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
# Declares that this table references a low-card table. This simply includes the
|
35
|
+
# LowCardTables::HasLowCardTable::Base method, and then calls that module's has_low_card_table method,
|
36
|
+
# which is the one that does all the real work.
|
37
|
+
def has_low_card_table(*args)
|
38
|
+
unless @_low_card_has_low_card_table_included
|
39
|
+
include LowCardTables::HasLowCardTable::Base
|
40
|
+
@_low_card_has_low_card_table_included = true
|
41
|
+
end
|
42
|
+
|
43
|
+
has_low_card_table(*args)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Does this model reference any low-card tables? This implementation just returns false -- if this is
|
47
|
+
# a low-card table, then it will have had the LowCardTables::HasLowCardTable::Base module included in
|
48
|
+
# after this one, and that implementation will return true.
|
49
|
+
def has_any_low_card_tables?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'low_card_tables/low_card_table/base'
|
4
|
+
require 'low_card_tables/has_low_card_table/base'
|
5
|
+
|
6
|
+
module LowCardTables
|
7
|
+
module ActiveRecord
|
8
|
+
# This module gets included into ::ActiveRecord::Migrations, and overrides key methods (using +alias_method_chain+)
|
9
|
+
# to add low-card support. Its job is to detect if a low-card table is being modified, and, if so:
|
10
|
+
#
|
11
|
+
# * Remove the all-columns unique index before the operation in question, and add it back afterwards
|
12
|
+
# * If a column has been removed, collapse any now-duplicate rows in question and update all referring tables
|
13
|
+
#
|
14
|
+
# It also adds a single method to migrations, #change_low_card_table, which does nothing of its own rather than
|
15
|
+
# to call the passed block -- but it does all the checking above at the start and end, and disables any such
|
16
|
+
# checking within the block. It thus gives you control over exactly when this happens.
|
17
|
+
module Migrations
|
18
|
+
extend ActiveSupport::Concern
|
19
|
+
|
20
|
+
# Overrides ::ActiveRecord::Migrations#create_table with low-cardinality support, as described in the comment
|
21
|
+
# for LowCardTables::ActiveRecord::Migrations.
|
22
|
+
def create_table_with_low_card_support(table_name, options = { }, &block)
|
23
|
+
::LowCardTables::ActiveRecord::Migrations.with_low_card_support(table_name, options) do |new_options|
|
24
|
+
create_table_without_low_card_support(table_name, new_options, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Overrides ::ActiveRecord::Migrations#add_column with low-cardinality support, as described in the comment
|
29
|
+
# for LowCardTables::ActiveRecord::Migrations.
|
30
|
+
def add_column_with_low_card_support(table_name, column_name, type, options = {})
|
31
|
+
::LowCardTables::ActiveRecord::Migrations.with_low_card_support(table_name, options) do |new_options|
|
32
|
+
add_column_without_low_card_support(table_name, column_name, type, options)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Overrides ::ActiveRecord::Migrations#remove_column with low-cardinality support, as described in the comment
|
37
|
+
# for LowCardTables::ActiveRecord::Migrations.
|
38
|
+
def remove_column_with_low_card_support(table_name, *column_names)
|
39
|
+
options = column_names.pop if column_names[-1] && column_names[-1].kind_of?(Hash)
|
40
|
+
::LowCardTables::ActiveRecord::Migrations.with_low_card_support(table_name, options) do |new_options|
|
41
|
+
args = [ table_name ]
|
42
|
+
args += column_names
|
43
|
+
args << new_options if new_options && new_options.size > 0
|
44
|
+
remove_column_without_low_card_support(*args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Overrides ::ActiveRecord::Migrations#change_table with low-cardinality support, as described in the comment
|
49
|
+
# for LowCardTables::ActiveRecord::Migrations.
|
50
|
+
def change_table_with_low_card_support(table_name, options = { }, &block)
|
51
|
+
::LowCardTables::ActiveRecord::Migrations.with_low_card_support(table_name, options) do |new_options|
|
52
|
+
ar = method(:change_table_without_low_card_support).arity
|
53
|
+
if ar > 1 || ar < -2
|
54
|
+
change_table_without_low_card_support(table_name, new_options, &block)
|
55
|
+
else
|
56
|
+
change_table_without_low_card_support(table_name, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Given the name of a low-card table and a block:
|
62
|
+
#
|
63
|
+
# * Removes the all-columns unique index for that low-card table;
|
64
|
+
# * Calls the block;
|
65
|
+
# * Looks for any removed columns, and, if so, collapses now-duplicate rows and updates all referrers;
|
66
|
+
# * Creates the all-columns unique index for that table.
|
67
|
+
#
|
68
|
+
# While inside the block, none of the above checking will be performed against that table, as it otherwise would
|
69
|
+
# be if you call #add_column, #remove_column, #create_table, or #change_table. This thus gives you a scope in
|
70
|
+
# which to do what you need to do, without the all-columns index interfering.
|
71
|
+
def change_low_card_table(table_name, &block)
|
72
|
+
::LowCardTables::ActiveRecord::Migrations.with_low_card_support(table_name, { :low_card => true }) do |new_options|
|
73
|
+
block.call
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
included do
|
78
|
+
alias_method_chain :create_table, :low_card_support
|
79
|
+
alias_method_chain :add_column, :low_card_support
|
80
|
+
alias_method_chain :remove_column, :low_card_support
|
81
|
+
alias_method_chain :change_table, :low_card_support
|
82
|
+
end
|
83
|
+
|
84
|
+
class << self
|
85
|
+
# Adds all the checking described in the comment on LowCardTables::ActiveRecord::Migrations for the given
|
86
|
+
# table name to the supplied block.
|
87
|
+
def with_low_card_support(table_name, options = { }, &block)
|
88
|
+
# Don't do this if we're already inside such a check -- this is needed because:
|
89
|
+
#
|
90
|
+
# change_table :foo do |t|
|
91
|
+
# t.remove :bar
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# ...actually translates internally to a call to remove_column inside change_table, and, otherwise, we'll
|
95
|
+
# try to do our work twice, which is bad news.
|
96
|
+
(options, low_card_options) = partition_low_card_options(options)
|
97
|
+
return block.call(options) if inside_migrations_check?
|
98
|
+
|
99
|
+
low_card_model = low_card_model_to_use_for(table_name, low_card_options)
|
100
|
+
return block.call(options) if (! low_card_model)
|
101
|
+
|
102
|
+
with_migrations_check do
|
103
|
+
without_unique_index(low_card_model, low_card_options) do
|
104
|
+
with_removed_column_detection(low_card_model, low_card_options) do
|
105
|
+
block.call(options)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
# Are we currently inside a call to #with_low_card_support?
|
113
|
+
def inside_migrations_check?
|
114
|
+
!! Thread.current[:_low_card_migrations_only_once]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Wrap the given block in code notifying us that we're inside a call to #with_low_card_support.
|
118
|
+
def with_migrations_check(&block)
|
119
|
+
begin
|
120
|
+
Thread.current[:_low_card_migrations_only_once] = true
|
121
|
+
block.call
|
122
|
+
ensure
|
123
|
+
Thread.current[:_low_card_migrations_only_once] = false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Wrap the given block in code that checks to see if we've removed any columns from the table that the given
|
128
|
+
# model is for, and, if so, calls low_card_collapse_rows_and_update_referrers! on that model.
|
129
|
+
def with_removed_column_detection(model, low_card_options, &block)
|
130
|
+
previous_columns = fresh_value_column_names(model)
|
131
|
+
|
132
|
+
begin
|
133
|
+
block.call
|
134
|
+
ensure
|
135
|
+
LowCardTables::VersionSupport.clear_schema_cache!(model)
|
136
|
+
model.reset_column_information
|
137
|
+
new_columns = fresh_value_column_names(model)
|
138
|
+
|
139
|
+
if (previous_columns - new_columns).length > 0
|
140
|
+
model.low_card_collapse_rows_and_update_referrers!(low_card_options)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Wrap the given block in code that removes the all-columns unique index on the given model (which must be
|
146
|
+
# a low-card table) beforehand, and recreates it afterwards.
|
147
|
+
def without_unique_index(model, low_card_options, &block)
|
148
|
+
begin
|
149
|
+
model.low_card_remove_unique_index!
|
150
|
+
block.call
|
151
|
+
ensure
|
152
|
+
unless low_card_options.has_key?(:low_card_collapse_rows) && (! low_card_options[:low_card_collapse_rows])
|
153
|
+
model.low_card_ensure_has_unique_index!(true)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Gets a fresh set of low_card_value_column_names from the given low-card model.
|
159
|
+
def fresh_value_column_names(model)
|
160
|
+
model.reset_column_information
|
161
|
+
model.low_card_value_column_names
|
162
|
+
end
|
163
|
+
|
164
|
+
# Splits an options Hash into two -- the first containing everything but low-card-related options, the second
|
165
|
+
# containing only low-card options.
|
166
|
+
def partition_low_card_options(options)
|
167
|
+
options = (options || { }).dup
|
168
|
+
low_card_options = { }
|
169
|
+
|
170
|
+
options.keys.each do |k|
|
171
|
+
if k.to_s =~ /^low_card/
|
172
|
+
low_card_options[k] = options.delete(k)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
[ options, low_card_options ]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Given a table name, looks to see if it's a low-card table -- either implicitly, because there's an existing
|
180
|
+
# model for that table that declares itself to be a low-card model, or explicitly, because we were passed
|
181
|
+
# :low_card => true in the options hash. If so, returns a model to use for that table -- either the existing
|
182
|
+
# model (implicit case) or a newly-created temporary model class (explicit case).
|
183
|
+
#
|
184
|
+
# Prefers an existing model over a temporary model, if there's both an existing model and we were passed
|
185
|
+
# :low_card => true.
|
186
|
+
def low_card_model_to_use_for(table_name, low_card_options)
|
187
|
+
out = existing_low_card_model_for(table_name)
|
188
|
+
out ||= temporary_model_class_for(table_name) if low_card_options[:low_card]
|
189
|
+
out
|
190
|
+
end
|
191
|
+
|
192
|
+
# Creates a temporary low-card model class for the given table_name. This is used only if we explicitly
|
193
|
+
# declare a model to be low-card in a migration, but there isn't currently a model for that table that
|
194
|
+
# declares itself to be low-card.
|
195
|
+
def temporary_model_class_for(table_name)
|
196
|
+
temporary_model_class = Class.new(::ActiveRecord::Base)
|
197
|
+
temporary_model_class.table_name = table_name
|
198
|
+
temporary_model_class.class_eval { is_low_card_table }
|
199
|
+
temporary_model_class.reset_column_information
|
200
|
+
temporary_model_class
|
201
|
+
end
|
202
|
+
|
203
|
+
# Looks at ::ActiveRecord::Base.descendants to see if there's an existing model class that references the given
|
204
|
+
# table_name and declares itself to be a low-card model class. If so, returns it.
|
205
|
+
#
|
206
|
+
# This method will attempt to eager-load all Rails code, so that we can detect the model class properly.
|
207
|
+
# (Otherwise, migrations typically don't end up loading most models, so, even if the model is there on disk,
|
208
|
+
# it will not be in memory and thus won't appear in ::ActiveRecord::Base.descendants.)
|
209
|
+
def existing_low_card_model_for(table_name)
|
210
|
+
# Make sure we load all models
|
211
|
+
::Rails.application.eager_load! if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application && ::Rails.application.respond_to?(:eager_load!)
|
212
|
+
out = ::ActiveRecord::Base.descendants.detect do |klass|
|
213
|
+
klass.table_name.strip.downcase == table_name.to_s.strip.downcase &&
|
214
|
+
klass.is_low_card_table? &&
|
215
|
+
klass.name && klass.name.strip.length > 0
|
216
|
+
end
|
217
|
+
|
218
|
+
out
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|