low_card_tables 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +59 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE +21 -0
  6. data/README.md +75 -0
  7. data/Rakefile +6 -0
  8. data/lib/low_card_tables.rb +72 -0
  9. data/lib/low_card_tables/active_record/base.rb +55 -0
  10. data/lib/low_card_tables/active_record/migrations.rb +223 -0
  11. data/lib/low_card_tables/active_record/relation.rb +35 -0
  12. data/lib/low_card_tables/active_record/scoping.rb +87 -0
  13. data/lib/low_card_tables/errors.rb +74 -0
  14. data/lib/low_card_tables/has_low_card_table/base.rb +114 -0
  15. data/lib/low_card_tables/has_low_card_table/low_card_association.rb +273 -0
  16. data/lib/low_card_tables/has_low_card_table/low_card_associations_manager.rb +143 -0
  17. data/lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb +224 -0
  18. data/lib/low_card_tables/has_low_card_table/low_card_objects_manager.rb +80 -0
  19. data/lib/low_card_tables/low_card_table/base.rb +184 -0
  20. data/lib/low_card_tables/low_card_table/cache.rb +214 -0
  21. data/lib/low_card_tables/low_card_table/cache_expiration/exponential_cache_expiration_policy.rb +151 -0
  22. data/lib/low_card_tables/low_card_table/cache_expiration/fixed_cache_expiration_policy.rb +23 -0
  23. data/lib/low_card_tables/low_card_table/cache_expiration/has_cache_expiration.rb +100 -0
  24. data/lib/low_card_tables/low_card_table/cache_expiration/no_caching_expiration_policy.rb +13 -0
  25. data/lib/low_card_tables/low_card_table/cache_expiration/unlimited_cache_expiration_policy.rb +13 -0
  26. data/lib/low_card_tables/low_card_table/row_collapser.rb +175 -0
  27. data/lib/low_card_tables/low_card_table/row_manager.rb +681 -0
  28. data/lib/low_card_tables/low_card_table/table_unique_index.rb +134 -0
  29. data/lib/low_card_tables/version.rb +4 -0
  30. data/lib/low_card_tables/version_support.rb +52 -0
  31. data/low_card_tables.gemspec +69 -0
  32. data/spec/low_card_tables/helpers/database_helper.rb +148 -0
  33. data/spec/low_card_tables/helpers/query_spy_helper.rb +47 -0
  34. data/spec/low_card_tables/helpers/system_helpers.rb +63 -0
  35. data/spec/low_card_tables/system/basic_system_spec.rb +254 -0
  36. data/spec/low_card_tables/system/bulk_system_spec.rb +334 -0
  37. data/spec/low_card_tables/system/caching_system_spec.rb +531 -0
  38. data/spec/low_card_tables/system/migrations_system_spec.rb +747 -0
  39. data/spec/low_card_tables/system/options_system_spec.rb +581 -0
  40. data/spec/low_card_tables/system/queries_system_spec.rb +142 -0
  41. data/spec/low_card_tables/system/validations_system_spec.rb +88 -0
  42. data/spec/low_card_tables/unit/active_record/base_spec.rb +53 -0
  43. data/spec/low_card_tables/unit/active_record/migrations_spec.rb +207 -0
  44. data/spec/low_card_tables/unit/active_record/relation_spec.rb +47 -0
  45. data/spec/low_card_tables/unit/active_record/scoping_spec.rb +101 -0
  46. data/spec/low_card_tables/unit/has_low_card_table/base_spec.rb +79 -0
  47. data/spec/low_card_tables/unit/has_low_card_table/low_card_association_spec.rb +287 -0
  48. data/spec/low_card_tables/unit/has_low_card_table/low_card_associations_manager_spec.rb +190 -0
  49. data/spec/low_card_tables/unit/has_low_card_table/low_card_dynamic_method_manager_spec.rb +234 -0
  50. data/spec/low_card_tables/unit/has_low_card_table/low_card_objects_manager_spec.rb +70 -0
  51. data/spec/low_card_tables/unit/low_card_table/base_spec.rb +207 -0
  52. data/spec/low_card_tables/unit/low_card_table/cache_expiration/exponential_cache_expiration_policy_spec.rb +128 -0
  53. data/spec/low_card_tables/unit/low_card_table/cache_expiration/fixed_cache_expiration_policy_spec.rb +25 -0
  54. data/spec/low_card_tables/unit/low_card_table/cache_expiration/has_cache_expiration_policy_spec.rb +100 -0
  55. data/spec/low_card_tables/unit/low_card_table/cache_expiration/no_caching_expiration_policy_spec.rb +14 -0
  56. data/spec/low_card_tables/unit/low_card_table/cache_expiration/unlimited_cache_expiration_policy_spec.rb +14 -0
  57. data/spec/low_card_tables/unit/low_card_table/cache_spec.rb +282 -0
  58. data/spec/low_card_tables/unit/low_card_table/row_collapser_spec.rb +109 -0
  59. data/spec/low_card_tables/unit/low_card_table/row_manager_spec.rb +918 -0
  60. data/spec/low_card_tables/unit/low_card_table/table_unique_index_spec.rb +117 -0
  61. 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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /spec_database_config.rb
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 &mdash; 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 &mdash; 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 &mdash; 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` &mdash; 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,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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