├── LICENSE ├── README.md ├── composite_primary_keys ├── .gitignore ├── History.txt ├── Manifest.txt ├── README.txt ├── README_DB2.txt ├── Rakefile ├── init.rb ├── install.rb ├── lib │ ├── adapter_helper │ │ ├── base.rb │ │ ├── mysql.rb │ │ ├── oracle.rb │ │ ├── oracle_enhanced.rb │ │ ├── postgresql.rb │ │ └── sqlite3.rb │ ├── composite_primary_keys.rb │ └── composite_primary_keys │ │ ├── association_preload.rb │ │ ├── associations.rb │ │ ├── attribute_methods.rb │ │ ├── base.rb │ │ ├── calculations.rb │ │ ├── composite_arrays.rb │ │ ├── connection_adapters │ │ ├── abstract_adapter.rb │ │ ├── ibm_db_adapter.rb │ │ ├── oracle_adapter.rb │ │ ├── oracle_enhanced_adapter.rb │ │ ├── postgresql_adapter.rb │ │ └── sqlite3_adapter.rb │ │ ├── fixtures.rb │ │ ├── migration.rb │ │ ├── reflection.rb │ │ ├── validations │ │ └── uniqueness.rb │ │ └── version.rb ├── loader.rb ├── local │ ├── database_connections.rb.sample │ ├── paths.rb.sample │ └── tasks.rb.sample ├── scripts │ ├── console.rb │ ├── txt2html │ └── txt2js ├── tasks │ ├── activerecord_selection.rake │ ├── databases.rake │ ├── databases │ │ ├── mysql.rake │ │ ├── oracle.rake │ │ ├── postgresql.rake │ │ └── sqlite3.rake │ ├── deployment.rake │ ├── local_setup.rake │ └── website.rake ├── test │ ├── README_tests.txt │ ├── abstract_unit.rb │ ├── connections │ │ ├── native_ibm_db │ │ │ └── connection.rb │ │ ├── native_mysql │ │ │ └── connection.rb │ │ ├── native_oracle │ │ │ └── connection.rb │ │ ├── native_oracle_enhanced │ │ │ └── connection.rb │ │ ├── native_postgresql │ │ │ └── connection.rb │ │ └── native_sqlite │ │ │ └── connection.rb │ ├── fixtures │ │ ├── article.rb │ │ ├── article_group.rb │ │ ├── article_groups.yml │ │ ├── articles.yml │ │ ├── comment.rb │ │ ├── comments.yml │ │ ├── db_definitions │ │ │ ├── db2-create-tables.sql │ │ │ ├── db2-drop-tables.sql │ │ │ ├── mysql.sql │ │ │ ├── oracle.drop.sql │ │ │ ├── oracle.sql │ │ │ ├── postgresql.sql │ │ │ └── sqlite.sql │ │ ├── department.rb │ │ ├── departments.yml │ │ ├── dorm.rb │ │ ├── dorms.yml │ │ ├── employee.rb │ │ ├── employees.yml │ │ ├── group.rb │ │ ├── groups.yml │ │ ├── hack.rb │ │ ├── hacks.yml │ │ ├── kitchen_sink.rb │ │ ├── kitchen_sinks.yml │ │ ├── membership.rb │ │ ├── membership_status.rb │ │ ├── membership_statuses.yml │ │ ├── memberships.yml │ │ ├── product.rb │ │ ├── product_tariff.rb │ │ ├── product_tariffs.yml │ │ ├── products.yml │ │ ├── reading.rb │ │ ├── readings.yml │ │ ├── reference_code.rb │ │ ├── reference_codes.yml │ │ ├── reference_type.rb │ │ ├── reference_types.yml │ │ ├── restaurant.rb │ │ ├── restaurants.yml │ │ ├── restaurants_suburbs.yml │ │ ├── room.rb │ │ ├── room_assignment.rb │ │ ├── room_assignments.yml │ │ ├── room_attribute.rb │ │ ├── room_attribute_assignment.rb │ │ ├── room_attribute_assignments.yml │ │ ├── room_attributes.yml │ │ ├── rooms.yml │ │ ├── seat.rb │ │ ├── seats.yml │ │ ├── street.rb │ │ ├── streets.yml │ │ ├── student.rb │ │ ├── students.yml │ │ ├── suburb.rb │ │ ├── suburbs.yml │ │ ├── tariff.rb │ │ ├── tariffs.yml │ │ ├── user.rb │ │ └── users.yml │ ├── hash_tricks.rb │ ├── plugins │ │ ├── pagination.rb │ │ └── pagination_helper.rb │ ├── test_associations.rb │ ├── test_attribute_methods.rb │ ├── test_attributes.rb │ ├── test_clone.rb │ ├── test_composite_arrays.rb │ ├── test_create.rb │ ├── test_delete.rb │ ├── test_dummy.rb │ ├── test_exists.rb │ ├── test_find.rb │ ├── test_ids.rb │ ├── test_miscellaneous.rb │ ├── test_pagination.rb │ ├── test_polymorphic.rb │ ├── test_santiago.rb │ ├── test_tutorial_examle.rb │ ├── test_update.rb │ └── test_validations.rb └── website │ ├── index.html │ ├── index.txt │ ├── javascripts │ └── rounded_corners_lite.inc.js │ ├── stylesheets │ └── screen.css │ ├── template.js │ ├── template.rhtml │ ├── version-raw.js │ ├── version-raw.txt │ ├── version.js │ └── version.txt └── mysql_to_postgresql /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2009-2010 Braintree Payment Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mysql to PostgreSQL 2 | 3 | This is a ruby script which migrates data from a MySQL database to PostgreSQL. It can be run as a 4 | one off migration, or as a delta script to copy newly modified data. 5 | 6 | ## Prerequisites 7 | 8 | * Designed to work on a Rails 2.x application 9 | * Majority of tables should have: 10 | * primary key column (usually called id, but can be changed) 11 | * updated_at column 12 | * index on updated_at 13 | 14 | ## Steps 15 | 16 | * Edit mysql_to_postgresql 17 | * Put in database credentials 18 | * Add any tables without an updated_at to TABLES_WITHOUT_UPDATED_AT 19 | * Update database.yml in your application to point to PostgreSQL 20 | * Create database tables by running: `rake db:create db:migrate` 21 | * Run ./mysql_to_postgresql 22 | 23 | ## License 24 | 25 | See the LICENSE file. 26 | -------------------------------------------------------------------------------- /composite_primary_keys/.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | tmp 3 | debug.log 4 | local/*.rb 5 | pkg 6 | -------------------------------------------------------------------------------- /composite_primary_keys/History.txt: -------------------------------------------------------------------------------- 1 | == 2.3.5.1 2010-02-13 2 | 3 | * Resolved "warning: already initialized constant HasManyThroughCantAssociateThroughHasManyReflection" [Titi Ala'ilima] 4 | 5 | == 2.3.5 2009-12-16 6 | 7 | * Fixed several bugs in has_one and has_many associations when :primary_key specified [kpumuk] 8 | 9 | == 2.3.2 2009-07-16 10 | 11 | * explicitly load associations.rb due to some getting an unitialized constant error 12 | 13 | == 2.3.2 2009-05-28 14 | 15 | * get tests working again with AR 2.3.2 16 | 17 | == 2.2.1 2009-01-21 18 | 19 | * fix ActiveRecord#exists? to work when passing conditions instead of ids 20 | 21 | == 2.2.0 2008-10-29 22 | 23 | * Rails 2.2.0 compatibility 24 | 25 | == 1.1.0 2008-10-29 26 | 27 | * fixes to get cpk working for Rails 2.1.2 28 | 29 | == 1.0.10 2008-10-22 30 | 31 | * add composite key where clause creator method [timurv] 32 | 33 | == 1.0.9 2008-09-08 34 | 35 | * fix postgres tests 36 | * fix for delete_records when has_many association has composite keys [darxriggs] 37 | * more consistent table/column name quoting [pbrant] 38 | 39 | == 1.0.8 2008-08-27 40 | 41 | * fix has_many :through for non composite models [thx rcarver] 42 | 43 | == 1.0.7 2008-08-12 44 | 45 | * fix for the last fix -- when has_many is composite and belongs_to is single 46 | 47 | == 1.0.6 2008-08-06 48 | 49 | * fix associations create 50 | 51 | == 1.0.5 2008-07-25 52 | 53 | * fix for calculations with a group by clause [thx Sirius Black] 54 | 55 | == 1.0.4 2008-07-15 56 | 57 | * support for oracle_enhanced adapter [thx Raimonds Simanovskis] 58 | 59 | == 1.0.3 2008-07-13 60 | 61 | * more fixes and tests for has many through [thx Menno van der Sman] 62 | 63 | == 1.0.2 2008-06-07 64 | 65 | * fix for has many through when through association has composite keys 66 | 67 | == 1.0.1 2008-06-06 68 | 69 | * Oracle fixes 70 | 71 | == 1.0.0 2008-06-05 72 | 73 | * Support for Rails 2.1 74 | 75 | == 0.9.93 2008-06-01 76 | 77 | * set fixed dependency on activerecord 2.0.2 78 | 79 | == 0.9.92 2008-02-22 80 | 81 | * Support for has_and_belongs_to_many 82 | 83 | == 0.9.91 2008-01-27 84 | 85 | * Incremented activerecord dependency to 2.0.2 [thx emmanuel.pirsch] 86 | 87 | == 0.9.90 2008-01-27 88 | 89 | * Trial release for rails/activerecord 2.0.2 supported 90 | 91 | == 0.9.1 2007-10-28 92 | 93 | * Migrations fix - allow :primary_key => [:name] to work [no unit test] [thx Shugo Maeda] 94 | 95 | == 0.9.0 2007-09-28 96 | 97 | * Added support for polymorphs [thx nerdrew] 98 | * init.rb file so gem can be installed as a plugin for Rails [thx nerdrew] 99 | * Added ibm_db support [thx K Venkatasubramaniyan] 100 | * Support for cleaning dependents [thx K Venkatasubramaniyan] 101 | * Rafactored db rake tasks into namespaces 102 | * Added namespaced tests (e.g. mysql:test for test_mysql) 103 | 104 | == 0.8.6 / 2007-6-12 105 | 106 | * 1 emergency fix due to Rails Core change 107 | * Rails v7004 removed #quote; fixed with connection.quote_column_name [thx nerdrew] 108 | 109 | == 0.8.5 / 2007-6-5 110 | 111 | * 1 change due to Rails Core change 112 | * Can no longer use RAILS_CONNECTION_ADAPTERS from Rails core 113 | * 7 dev improvement: 114 | * Changed History.txt syntax to rdoc format 115 | * Added deploy tasks 116 | * Removed CHANGELOG + migrated into History.txt 117 | * Changed PKG_NAME -> GEM_NAME in Rakefile 118 | * Renamed README -> README.txt for :publish_docs task 119 | * Added :check_version task 120 | * VER => VERS in rakefile 121 | * 1 website improvement: 122 | * website/index.txt includes link to "8 steps to fix other ppls code" 123 | 124 | == 0.8.4 / 2007-5-3 125 | 126 | * 1 bugfix 127 | * Corrected ids_list => ids in the exception message. That'll teach me for not adding unit tests before fixing bugs. 128 | 129 | == 0.8.3 / 2007-5-3 130 | 131 | * 1 bugfix 132 | * Explicit reference to ::ActiveRecord::RecordNotFound 133 | * 1 website addition: 134 | * Added routing help [Pete Sumskas] 135 | 136 | == 0.8.2 / 2007-4-11 137 | 138 | * 1 major enhancement: 139 | * Oracle unit tests!! [Darrin Holst] 140 | * And they work too 141 | 142 | == 0.8.1 / 2007-4-10 143 | 144 | * 1 bug fix: 145 | * Fixed the distinct(count) for oracle (removed 'as') 146 | 147 | == 0.8.0 / 2007-4-6 148 | 149 | * 1 major enhancement: 150 | * Support for calcualtions on associations 151 | * 2 new DB supported: 152 | * Tests run on sqlite 153 | * Tests run on postgresql 154 | * History.txt to keep track of changes like these 155 | * Using Hoe for Rakefile 156 | * Website generator rake tasks 157 | 158 | == 0.3.3 159 | * id= 160 | * create now work 161 | 162 | == 0.1.4 163 | * it was important that #{primary_key} for composites --> 'key1,key2' and not 'key1key2' so created PrimaryKeys class 164 | 165 | == 0.0.1 166 | * Initial version 167 | * set_primary_keys(*keys) is the activation class method to transform an ActiveRecord into a composite primary key AR 168 | * find(*ids) supports the passing of 169 | * id sets: Foo.find(2,1), 170 | * lists of id sets: Foo.find([2,1], [7,3], [8,12]), 171 | * and even stringified versions of the above: 172 | * Foo.find '2,1' or Foo.find '2,1;7,3' 173 | -------------------------------------------------------------------------------- /composite_primary_keys/Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | Manifest.txt 3 | README.txt 4 | README_DB2.txt 5 | Rakefile 6 | init.rb 7 | install.rb 8 | lib/adapter_helper/base.rb 9 | lib/adapter_helper/mysql.rb 10 | lib/adapter_helper/oracle.rb 11 | lib/adapter_helper/postgresql.rb 12 | lib/adapter_helper/sqlite3.rb 13 | lib/composite_primary_keys.rb 14 | lib/composite_primary_keys/association_preload.rb 15 | lib/composite_primary_keys/associations.rb 16 | lib/composite_primary_keys/attribute_methods.rb 17 | lib/composite_primary_keys/base.rb 18 | lib/composite_primary_keys/calculations.rb 19 | lib/composite_primary_keys/composite_arrays.rb 20 | lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb 21 | lib/composite_primary_keys/connection_adapters/oracle_adapter.rb 22 | lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb 23 | lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb 24 | lib/composite_primary_keys/fixtures.rb 25 | lib/composite_primary_keys/migration.rb 26 | lib/composite_primary_keys/reflection.rb 27 | lib/composite_primary_keys/validations/uniqueness.rb 28 | lib/composite_primary_keys/version.rb 29 | loader.rb 30 | local/database_connections.rb.sample 31 | local/paths.rb.sample 32 | local/tasks.rb.sample 33 | scripts/console.rb 34 | scripts/txt2html 35 | scripts/txt2js 36 | tasks/activerecord_selection.rake 37 | tasks/databases.rake 38 | tasks/databases/mysql.rake 39 | tasks/databases/oracle.rake 40 | tasks/databases/postgresql.rake 41 | tasks/databases/sqlite3.rake 42 | tasks/deployment.rake 43 | tasks/local_setup.rake 44 | tasks/website.rake 45 | test/README_tests.txt 46 | test/abstract_unit.rb 47 | test/connections/native_ibm_db/connection.rb 48 | test/connections/native_mysql/connection.rb 49 | test/connections/native_oracle/connection.rb 50 | test/connections/native_postgresql/connection.rb 51 | test/connections/native_sqlite/connection.rb 52 | test/fixtures/article.rb 53 | test/fixtures/articles.yml 54 | test/fixtures/comment.rb 55 | test/fixtures/comments.yml 56 | test/fixtures/db_definitions/db2-create-tables.sql 57 | test/fixtures/db_definitions/db2-drop-tables.sql 58 | test/fixtures/db_definitions/mysql.sql 59 | test/fixtures/db_definitions/oracle.drop.sql 60 | test/fixtures/db_definitions/oracle.sql 61 | test/fixtures/db_definitions/postgresql.sql 62 | test/fixtures/db_definitions/sqlite.sql 63 | test/fixtures/department.rb 64 | test/fixtures/departments.yml 65 | test/fixtures/employee.rb 66 | test/fixtures/employees.yml 67 | test/fixtures/group.rb 68 | test/fixtures/groups.yml 69 | test/fixtures/hack.rb 70 | test/fixtures/hacks.yml 71 | test/fixtures/membership.rb 72 | test/fixtures/membership_status.rb 73 | test/fixtures/membership_statuses.yml 74 | test/fixtures/memberships.yml 75 | test/fixtures/product.rb 76 | test/fixtures/product_tariff.rb 77 | test/fixtures/product_tariffs.yml 78 | test/fixtures/products.yml 79 | test/fixtures/reading.rb 80 | test/fixtures/readings.yml 81 | test/fixtures/reference_code.rb 82 | test/fixtures/reference_codes.yml 83 | test/fixtures/reference_type.rb 84 | test/fixtures/reference_types.yml 85 | test/fixtures/street.rb 86 | test/fixtures/streets.yml 87 | test/fixtures/suburb.rb 88 | test/fixtures/suburbs.yml 89 | test/fixtures/tariff.rb 90 | test/fixtures/tariffs.yml 91 | test/fixtures/user.rb 92 | test/fixtures/users.yml 93 | test/hash_tricks.rb 94 | test/plugins/pagination.rb 95 | test/plugins/pagination_helper.rb 96 | test/test_associations.rb 97 | test/test_attribute_methods.rb 98 | test/test_attributes.rb 99 | test/test_clone.rb 100 | test/test_composite_arrays.rb 101 | test/test_create.rb 102 | test/test_delete.rb 103 | test/test_dummy.rb 104 | test/test_exists.rb 105 | test/test_find.rb 106 | test/test_ids.rb 107 | test/test_miscellaneous.rb 108 | test/test_pagination.rb 109 | test/test_polymorphic.rb 110 | test/test_santiago.rb 111 | test/test_tutorial_examle.rb 112 | test/test_update.rb 113 | tmp/test.db 114 | website/index.html 115 | website/index.txt 116 | website/javascripts/rounded_corners_lite.inc.js 117 | website/stylesheets/screen.css 118 | website/template.js 119 | website/template.rhtml 120 | website/version-raw.js 121 | website/version-raw.txt 122 | website/version.js 123 | website/version.txt 124 | -------------------------------------------------------------------------------- /composite_primary_keys/README.txt: -------------------------------------------------------------------------------- 1 | = Composite Primary Keys for ActiveRecords 2 | 3 | == Summary 4 | 5 | ActiveRecords/Rails famously doesn't support composite primary keys. 6 | This RubyGem extends the activerecord gem to provide CPK support. 7 | 8 | == Installation 9 | 10 | gem install composite_primary_keys 11 | 12 | == Usage 13 | 14 | require 'composite_primary_keys' 15 | class ProductVariation 16 | set_primary_keys :product_id, :variation_seq 17 | end 18 | 19 | pv = ProductVariation.find(345, 12) 20 | 21 | It even supports composite foreign keys for associations. 22 | 23 | See http://compositekeys.rubyforge.org for more. 24 | 25 | == Running Tests 26 | 27 | See test/README.tests.txt 28 | 29 | == Url 30 | 31 | http://compositekeys.rubyforge.org 32 | 33 | == Questions, Discussion and Contributions 34 | 35 | http://groups.google.com/compositekeys 36 | 37 | == Author 38 | 39 | Written by Dr Nic Williams, drnicwilliams@gmail 40 | Contributions by many! 41 | 42 | -------------------------------------------------------------------------------- /composite_primary_keys/README_DB2.txt: -------------------------------------------------------------------------------- 1 | Composite Primary key support for db2 2 | 3 | == Driver Support 4 | 5 | DB2 support requires the IBM_DB driver provided by http://rubyforge.org/projects/rubyibm/ 6 | project. Install using gem install ibm_db. Tested against version 0.60 of the driver. 7 | This rubyforge project appears to be permenant location for the IBM adapter. 8 | Older versions of the driver available from IBM Alphaworks will not work. 9 | 10 | == Driver Bug and workaround provided as part of this plugin 11 | 12 | Unlike the basic quote routine available for Rails AR, the DB2 adapter's quote 13 | method doesn't return " column_name = 1 " when string values (integers in string type variable) 14 | are passed for quoting numeric column. Rather it returns "column_name = '1'. 15 | DB2 doesn't accept single quoting numeric columns in SQL. Currently, as part of 16 | this plugin a fix is provided for the DB2 adapter since this plugin does 17 | pass string values like this. Perhaps a patch should be sent to the DB2 adapter 18 | project for a permanant fix. 19 | 20 | == Database Setup 21 | 22 | Database must be manually created using a separate command. Read the rake task 23 | for creating tables and change the db name, user and passwords accordingly. 24 | 25 | == Tested Database Server version 26 | 27 | This is tested against DB2 v9.1 in Ubuntu Feisty Fawn (7.04) 28 | 29 | == Tested Database Client version 30 | 31 | This is tested against DB2 v9.1 in Ubuntu Feisty Fawn (7.04) 32 | 33 | 34 | -------------------------------------------------------------------------------- /composite_primary_keys/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/clean' 4 | require 'rake/testtask' 5 | require 'rake/rdoctask' 6 | require 'rake/packagetask' 7 | require 'rake/gempackagetask' 8 | require 'rake/contrib/rubyforgepublisher' 9 | require 'fileutils' 10 | require 'hoe' 11 | include FileUtils 12 | require File.join(File.dirname(__FILE__), 'lib', 'composite_primary_keys', 'version') 13 | 14 | AUTHOR = "Dr Nic Williams" 15 | EMAIL = "drnicwilliams@gmail.com" 16 | DESCRIPTION = "Composite key support for ActiveRecords" 17 | GEM_NAME = "composite_primary_keys" # what ppl will type to install your gem 18 | if File.exists?("~/.rubyforge/user-config.yml") 19 | # TODO this should prob go in a local/ file 20 | config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml"))) 21 | RUBYFORGE_USERNAME = config["username"] 22 | end 23 | RUBYFORGE_PROJECT = "compositekeys" 24 | HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org" 25 | 26 | REV = nil #File.read(".svn/entries")[/committed-rev="(\d+)"/, 1] rescue nil 27 | VERS = ENV['VERSION'] || (CompositePrimaryKeys::VERSION::STRING + (REV ? ".#{REV}" : "")) 28 | CLEAN.include ['**/.*.sw?', '*.gem', '.config','debug.log','*.db','logfile','log/**/*','**/.DS_Store', '.project'] 29 | RDOC_OPTS = ['--quiet', '--title', "newgem documentation", 30 | "--opname", "index.html", 31 | "--line-numbers", 32 | "--main", "README", 33 | "--inline-source"] 34 | 35 | class Hoe 36 | def extra_deps 37 | @extra_deps.reject { |x| Array(x).first == 'hoe' } 38 | end 39 | end 40 | 41 | # Generate all the Rake tasks 42 | # Run 'rake -T' to see list of generated tasks (from gem root directory) 43 | hoe = Hoe.new(GEM_NAME, VERS) do |p| 44 | p.author = AUTHOR 45 | p.description = DESCRIPTION 46 | p.email = EMAIL 47 | p.summary = DESCRIPTION 48 | p.url = HOMEPATH 49 | p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT 50 | p.test_globs = ["test/**/test*.rb"] 51 | p.clean_globs |= CLEAN #An array of file patterns to delete on clean. 52 | 53 | # == Optional 54 | p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n") 55 | p.extra_deps = [['activerecord', '>= 2.3.5']] #An array of rubygem dependencies. 56 | #p.spec_extras - A hash of extra values to set in the gemspec. 57 | end 58 | 59 | CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n") 60 | PATH = RUBYFORGE_PROJECT 61 | hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc') 62 | 63 | PROJECT_ROOT = File.expand_path(".") 64 | 65 | require 'loader' 66 | -------------------------------------------------------------------------------- /composite_primary_keys/init.rb: -------------------------------------------------------------------------------- 1 | # Include hook code here 2 | require_dependency 'composite_primary_keys' 3 | -------------------------------------------------------------------------------- /composite_primary_keys/install.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'find' 3 | require 'ftools' 4 | 5 | include Config 6 | 7 | # this was adapted from rdoc's install.rb by ways of Log4r 8 | 9 | $sitedir = CONFIG["sitelibdir"] 10 | unless $sitedir 11 | version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] 12 | $libdir = File.join(CONFIG["libdir"], "ruby", version) 13 | $sitedir = $:.find {|x| x =~ /site_ruby/ } 14 | if !$sitedir 15 | $sitedir = File.join($libdir, "site_ruby") 16 | elsif $sitedir !~ Regexp.quote(version) 17 | $sitedir = File.join($sitedir, version) 18 | end 19 | end 20 | 21 | # the acual gruntwork 22 | Dir.chdir("lib") 23 | 24 | Find.find("composite_primary_keys", "composite_primary_keys.rb") { |f| 25 | if f[-3..-1] == ".rb" 26 | File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) 27 | else 28 | File::makedirs(File.join($sitedir, *f.split(/\//))) 29 | end 30 | } 31 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/adapter_helper/base.rb: -------------------------------------------------------------------------------- 1 | module AdapterHelper 2 | class Base 3 | class << self 4 | attr_accessor :adapter 5 | 6 | def load_connection_from_env(adapter) 7 | self.adapter = adapter 8 | unless ENV['cpk_adapters'] 9 | puts error_msg_setup_helper 10 | exit 11 | end 12 | 13 | ActiveRecord::Base.configurations = YAML.load(ENV['cpk_adapters']) 14 | unless spec = ActiveRecord::Base.configurations[adapter] 15 | puts error_msg_adapter_helper 16 | exit 17 | end 18 | spec[:adapter] = adapter 19 | spec 20 | end 21 | 22 | def error_msg_setup_helper 23 | <<-EOS 24 | Setup Helper: 25 | CPK now has a place for your individual testing configuration. 26 | That is, instead of hardcoding it in the Rakefile and test/connections files, 27 | there is now a local/database_connections.rb file that is NOT in the 28 | repository. Your personal DB information (username, password etc) can 29 | be stored here without making it difficult to submit patches etc. 30 | 31 | Installation: 32 | i) cp locals/database_connections.rb.sample locals/database_connections.rb 33 | ii) For #{adapter} connection details see "Adapter Setup Helper" below. 34 | iii) Rerun this task 35 | 36 | #{error_msg_adapter_helper} 37 | 38 | Current ENV: 39 | #{ENV.inspect} 40 | EOS 41 | end 42 | 43 | def error_msg_adapter_helper 44 | <<-EOS 45 | Adapter Setup Helper: 46 | To run #{adapter} tests, you need to setup your #{adapter} connections. 47 | In your local/database_connections.rb file, within the ENV['cpk_adapter'] hash, add: 48 | "#{adapter}" => { adapter settings } 49 | 50 | That is, it will look like: 51 | ENV['cpk_adapters'] = { 52 | "#{adapter}" => { 53 | :adapter => "#{adapter}", 54 | :username => "root", 55 | :password => "root", 56 | # ... 57 | } 58 | }.to_yaml 59 | EOS 60 | end 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/adapter_helper/mysql.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'base') 2 | 3 | module AdapterHelper 4 | class MySQL < Base 5 | class << self 6 | def load_connection_from_env 7 | spec = super('mysql') 8 | spec[:database] ||= 'composite_primary_keys_unittest' 9 | spec 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/adapter_helper/oracle.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'base') 2 | 3 | module AdapterHelper 4 | class Oracle < Base 5 | class << self 6 | def load_connection_from_env 7 | spec = super('oracle') 8 | spec 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/adapter_helper/oracle_enhanced.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'base') 2 | 3 | module AdapterHelper 4 | class OracleEnhanced < Base 5 | class << self 6 | def load_connection_from_env 7 | spec = super('oracle_enhanced') 8 | spec 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/adapter_helper/postgresql.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'base') 2 | 3 | module AdapterHelper 4 | class Postgresql < Base 5 | class << self 6 | def load_connection_from_env 7 | spec = super('postgresql') 8 | spec[:database] ||= 'composite_primary_keys_unittest' 9 | spec 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/adapter_helper/sqlite3.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'base') 2 | 3 | module AdapterHelper 4 | class Sqlite3 < Base 5 | class << self 6 | def load_connection_from_env 7 | spec = super('sqlite3') 8 | spec[:dbfile] ||= "tmp/test.db" 9 | spec 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Copyright (c) 2006 Nic Williams 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | #++ 23 | 24 | $:.unshift(File.dirname(__FILE__)) unless 25 | $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) 26 | 27 | unless defined?(ActiveRecord) 28 | begin 29 | require 'active_record' 30 | rescue LoadError 31 | require 'rubygems' 32 | require_gem 'activerecord' 33 | end 34 | end 35 | 36 | require 'active_record/associations.rb' 37 | 38 | require 'composite_primary_keys/fixtures' 39 | require 'composite_primary_keys/composite_arrays' 40 | require 'composite_primary_keys/associations' 41 | require 'composite_primary_keys/association_preload' 42 | require 'composite_primary_keys/reflection' 43 | require 'composite_primary_keys/base' 44 | require 'composite_primary_keys/calculations' 45 | require 'composite_primary_keys/migration' 46 | require 'composite_primary_keys/attribute_methods' 47 | require 'composite_primary_keys/validations/uniqueness' 48 | 49 | ActiveRecord::Base.class_eval do 50 | include CompositePrimaryKeys::ActiveRecord::Base 51 | end 52 | 53 | Dir[File.dirname(__FILE__) + '/composite_primary_keys/connection_adapters/*.rb'].each do |adapter| 54 | begin 55 | require adapter.gsub('.rb','') 56 | rescue MissingSourceFile 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/association_preload.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | module ActiveRecord 3 | module AssociationPreload 4 | def self.append_features(base) 5 | super 6 | base.send(:extend, ClassMethods) 7 | end 8 | 9 | # Composite key versions of Association functions 10 | module ClassMethods 11 | def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) 12 | table_name = reflection.klass.quoted_table_name 13 | id_to_record_map, ids = construct_id_map_for_composite(records) 14 | records.each {|record| record.send(reflection.name).loaded} 15 | options = reflection.options 16 | 17 | if composite? 18 | primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP) 19 | where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys| 20 | "(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")" 21 | end.join(" OR ") 22 | 23 | conditions = [where, ids].flatten 24 | joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection, reflection.klass.table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}" 25 | parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{connection.quote_column_name(k)}"} 26 | parent_record_id = connection.concat(*parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact) 27 | else 28 | conditions = ["t0.#{connection.quote_column_name(reflection.primary_key_name)} IN (?)", ids] 29 | joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{connection.quote_column_name(reflection.klass.primary_key)} = t0.#{connection.quote_column_name(reflection.association_foreign_key)})" 30 | parent_record_id = reflection.primary_key_name 31 | end 32 | 33 | conditions.first << append_conditions(reflection, preload_options) 34 | 35 | associated_records = reflection.klass.find(:all, 36 | :conditions => conditions, 37 | :include => options[:include], 38 | :joins => joins, 39 | :select => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_", 40 | :order => options[:order]) 41 | 42 | set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_') 43 | end 44 | 45 | def preload_has_many_association(records, reflection, preload_options={}) 46 | id_to_record_map, ids = construct_id_map_for_composite(records) 47 | records.each {|record| record.send(reflection.name).loaded} 48 | options = reflection.options 49 | 50 | if options[:through] 51 | through_records = preload_through_records(records, reflection, options[:through]) 52 | through_reflection = reflections[options[:through]] 53 | through_primary_key = through_reflection.primary_key_name 54 | 55 | unless through_records.empty? 56 | source = reflection.source_reflection.name 57 | #add conditions from reflection! 58 | through_records.first.class.preload_associations(through_records, source, reflection.options) 59 | through_records.each do |through_record| 60 | key = through_primary_key.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| through_record.send(k)}.join(CompositePrimaryKeys::ID_SEP) 61 | add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source)) 62 | end 63 | end 64 | else 65 | associated_records = find_associated_records(ids, reflection, preload_options) 66 | set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)) 67 | end 68 | end 69 | 70 | def preload_through_records(records, reflection, through_association) 71 | through_reflection = reflections[through_association] 72 | through_primary_key = through_reflection.primary_key_name 73 | 74 | if reflection.options[:source_type] 75 | interface = reflection.source_reflection.options[:foreign_type] 76 | preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} 77 | 78 | records.compact! 79 | records.first.class.preload_associations(records, through_association, preload_options) 80 | 81 | # Dont cache the association - we would only be caching a subset 82 | through_records = [] 83 | records.each do |record| 84 | proxy = record.send(through_association) 85 | 86 | if proxy.respond_to?(:target) 87 | through_records << proxy.target 88 | proxy.reset 89 | else # this is a has_one :through reflection 90 | through_records << proxy if proxy 91 | end 92 | end 93 | through_records.flatten! 94 | else 95 | records.first.class.preload_associations(records, through_association) 96 | through_records = records.map {|record| record.send(through_association)}.flatten 97 | end 98 | 99 | through_records.compact! 100 | through_records 101 | end 102 | 103 | def preload_belongs_to_association(records, reflection, preload_options={}) 104 | options = reflection.options 105 | primary_key_name = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP) 106 | 107 | if options[:polymorphic] 108 | raise AssociationNotSupported, "Polymorphic joins not supported for composite keys" 109 | else 110 | # I need to keep the original ids for each record (as opposed to the stringified) so 111 | # that they get properly converted for each db so the id_map ends up looking like: 112 | # 113 | # { '1,2' => {:id => [1,2], :records => [...records...]}} 114 | id_map = {} 115 | 116 | records.each do |record| 117 | key = primary_key_name.map{|k| record.send(k)} 118 | key_as_string = key.join(CompositePrimaryKeys::ID_SEP) 119 | 120 | if key_as_string 121 | mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []}) 122 | mapped_records[:records] << record 123 | end 124 | end 125 | 126 | 127 | klasses_and_ids = [[reflection.klass.name, id_map]] 128 | end 129 | 130 | klasses_and_ids.each do |klass_and_id| 131 | klass_name, id_map = *klass_and_id 132 | klass = klass_name.constantize 133 | table_name = klass.quoted_table_name 134 | connection = reflection.active_record.connection 135 | 136 | if composite? 137 | primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP) 138 | ids = id_map.keys.uniq.map {|id| id_map[id][:id]} 139 | 140 | where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys| 141 | "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")" 142 | end.join(" OR ") 143 | 144 | conditions = [where, ids].flatten 145 | else 146 | conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq] 147 | end 148 | 149 | conditions.first << append_conditions(reflection, preload_options) 150 | 151 | associated_records = klass.find(:all, 152 | :conditions => conditions, 153 | :include => options[:include], 154 | :select => options[:select], 155 | :joins => options[:joins], 156 | :order => options[:order]) 157 | 158 | set_association_single_records(id_map, reflection.name, associated_records, primary_key) 159 | end 160 | end 161 | 162 | def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) 163 | associated_records.each do |associated_record| 164 | associated_record_key = associated_record[key] 165 | associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s 166 | mapped_records = id_to_record_map[associated_record_key] 167 | add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record) 168 | end 169 | end 170 | 171 | def set_association_single_records(id_to_record_map, reflection_name, associated_records, key) 172 | seen_keys = {} 173 | associated_records.each do |associated_record| 174 | associated_record_key = associated_record[key] 175 | associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s 176 | 177 | #this is a has_one or belongs_to: there should only be one record. 178 | #Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please 179 | # only one row per distinct foo_id' so this where we enforce that 180 | next if seen_keys[associated_record_key] 181 | seen_keys[associated_record_key] = true 182 | mapped_records = id_to_record_map[associated_record_key][:records] 183 | mapped_records.each do |mapped_record| 184 | mapped_record.send("set_#{reflection_name}_target", associated_record) 185 | end 186 | end 187 | end 188 | 189 | def find_associated_records(ids, reflection, preload_options) 190 | options = reflection.options 191 | table_name = reflection.klass.quoted_table_name 192 | 193 | if interface = reflection.options[:as] 194 | raise AssociationNotSupported, "Polymorphic joins not supported for composite keys" 195 | else 196 | connection = reflection.active_record.connection 197 | foreign_key = reflection.primary_key_name 198 | conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids] 199 | 200 | if composite? 201 | foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP) 202 | 203 | where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys| 204 | "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")" 205 | end.join(" OR ") 206 | 207 | conditions = [where, ids].flatten 208 | end 209 | end 210 | 211 | conditions.first << append_conditions(reflection, preload_options) 212 | 213 | reflection.klass.find(:all, 214 | :select => (preload_options[:select] || options[:select] || "#{table_name}.*"), 215 | :include => preload_options[:include] || options[:include], 216 | :conditions => conditions, 217 | :joins => options[:joins], 218 | :group => preload_options[:group] || options[:group], 219 | :order => preload_options[:order] || options[:order]) 220 | end 221 | 222 | # Given a collection of ActiveRecord objects, constructs a Hash which maps 223 | # the objects' IDs to the relevant objects. Returns a 2-tuple 224 | # (id_to_record_map, ids) where +id_to_record_map+ is the Hash, 225 | # and +ids+ is an Array of record IDs. 226 | def construct_id_map_for_composite(records) 227 | id_to_record_map = {} 228 | ids = [] 229 | records.each do |record| 230 | primary_key ||= record.class.primary_key 231 | ids << record.id 232 | mapped_records = (id_to_record_map[record.id.to_s] ||= []) 233 | mapped_records << record 234 | end 235 | ids.uniq! 236 | return id_to_record_map, ids 237 | end 238 | 239 | def full_composite_join_clause(reflection, table1, full_keys1, table2, full_keys2) 240 | connection = reflection.active_record.connection 241 | full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String) 242 | full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String) 243 | where_clause = [full_keys1, full_keys2].transpose.map do |key_pair| 244 | quoted1 = connection.quote_table_name(table1) 245 | quoted2 = connection.quote_table_name(table2) 246 | "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}" 247 | end.join(" AND ") 248 | "(#{where_clause})" 249 | end 250 | end 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/attribute_methods.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | module ActiveRecord 3 | module AttributeMethods #:nodoc: 4 | def self.append_features(base) 5 | super 6 | base.send(:extend, ClassMethods) 7 | end 8 | 9 | module ClassMethods 10 | # Define an attribute reader method. Cope with nil column. 11 | def define_read_method(symbol, attr_name, column) 12 | cast_code = column.type_cast_code('v') if column 13 | cast_code = "::#{cast_code}" if cast_code && cast_code.match('ActiveRecord::.*') 14 | access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" 15 | 16 | unless self.primary_keys.include?(attr_name.to_sym) 17 | access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") 18 | end 19 | 20 | if cache_attribute?(attr_name) 21 | access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" 22 | end 23 | 24 | evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end" 25 | end 26 | 27 | # Evaluate the definition for an attribute related method 28 | def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name) 29 | unless primary_keys.include?(method_name.to_sym) 30 | generated_methods << method_name 31 | end 32 | 33 | begin 34 | class_eval(method_definition, __FILE__, __LINE__) 35 | rescue SyntaxError => err 36 | generated_methods.delete(attr_name) 37 | if logger 38 | logger.warn "Exception occurred during reader method compilation." 39 | logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" 40 | logger.warn "#{err.message}" 41 | end 42 | end 43 | end 44 | end 45 | 46 | # Allows access to the object attributes, which are held in the @attributes hash, as though they 47 | # were first-class methods. So a Person class with a name attribute can use Person#name and 48 | # Person#name= and never directly use the attributes hash -- except for multiple assigns with 49 | # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that 50 | # the completed attribute is not nil or 0. 51 | # 52 | # It's also possible to instantiate related objects, so a Client class belonging to the clients 53 | # table with a master_id foreign key can instantiate master through Client#master. 54 | def method_missing(method_id, *args, &block) 55 | method_name = method_id.to_s 56 | 57 | # If we haven't generated any methods yet, generate them, then 58 | # see if we've created the method we're looking for. 59 | if !self.class.generated_methods? 60 | self.class.define_attribute_methods 61 | 62 | if self.class.generated_methods.include?(method_name) 63 | return self.send(method_id, *args, &block) 64 | end 65 | end 66 | 67 | if self.class.primary_keys.include?(method_name.to_sym) 68 | ids[self.class.primary_keys.index(method_name.to_sym)] 69 | elsif md = self.class.match_attribute_method?(method_name) 70 | attribute_name, method_type = md.pre_match, md.to_s 71 | if @attributes.include?(attribute_name) 72 | __send__("attribute#{method_type}", attribute_name, *args, &block) 73 | else 74 | super 75 | end 76 | elsif @attributes.include?(method_name) 77 | read_attribute(method_name) 78 | else 79 | super 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/base.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | module ActiveRecord #:nodoc: 3 | class CompositeKeyError < StandardError #:nodoc: 4 | end 5 | 6 | module Base #:nodoc: 7 | 8 | INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys' 9 | NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet' 10 | 11 | def self.append_features(base) 12 | super 13 | base.send(:include, InstanceMethods) 14 | base.extend(ClassMethods) 15 | end 16 | 17 | module ClassMethods 18 | def set_primary_keys(*keys) 19 | keys = keys.first if keys.first.is_a?(Array) 20 | keys = keys.map { |k| k.to_sym } 21 | cattr_accessor :primary_keys 22 | self.primary_keys = keys.to_composite_keys 23 | 24 | class_eval <<-EOV 25 | extend CompositeClassMethods 26 | include CompositeInstanceMethods 27 | 28 | include CompositePrimaryKeys::ActiveRecord::Associations 29 | include CompositePrimaryKeys::ActiveRecord::AssociationPreload 30 | include CompositePrimaryKeys::ActiveRecord::Calculations 31 | include CompositePrimaryKeys::ActiveRecord::AttributeMethods 32 | 33 | extend CompositePrimaryKeys::ActiveRecord::Validations::Uniqueness::ClassMethods 34 | EOV 35 | end 36 | 37 | def composite? 38 | false 39 | end 40 | end 41 | 42 | module InstanceMethods 43 | def composite?; self.class.composite?; end 44 | end 45 | 46 | module CompositeInstanceMethods 47 | 48 | # A model instance's primary keys is always available as model.ids 49 | # whether you name it the default 'id' or set it to something else. 50 | def id 51 | attr_names = self.class.primary_keys 52 | CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) }) 53 | end 54 | alias_method :ids, :id 55 | 56 | def to_param 57 | id.to_s 58 | end 59 | 60 | def id_before_type_cast #:nodoc: 61 | raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET 62 | end 63 | 64 | def quoted_id #:nodoc: 65 | [self.class.primary_keys, ids]. 66 | transpose. 67 | map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}. 68 | to_composite_ids 69 | end 70 | 71 | # Sets the primary ID. 72 | def id=(ids) 73 | ids = ids.split(ID_SEP) if ids.is_a?(String) 74 | ids.flatten! 75 | unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length 76 | raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids" 77 | end 78 | [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)} 79 | id 80 | end 81 | 82 | # Returns a clone of the record that hasn't been assigned an id yet and 83 | # is treated as a new record. Note that this is a "shallow" clone: 84 | # it copies the object's attributes only, not its associations. 85 | # The extent of a "deep" clone is application-specific and is therefore 86 | # left to the application to implement according to its need. 87 | def clone 88 | attrs = self.attributes_before_type_cast 89 | self.class.primary_keys.each {|key| attrs.delete(key.to_s)} 90 | self.class.new do |record| 91 | record.send :instance_variable_set, '@attributes', attrs 92 | end 93 | end 94 | 95 | 96 | private 97 | # The xx_without_callbacks methods are overwritten as that is the end of the alias chain 98 | 99 | # Creates a new record with values matching those of the instance attributes. 100 | def create_without_callbacks 101 | unless self.id 102 | raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values" 103 | end 104 | attributes_minus_pks = attributes_with_quotes(false) 105 | quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) } 106 | cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns 107 | vals = attributes_minus_pks.values << quoted_id 108 | connection.insert( 109 | "INSERT INTO #{self.class.quoted_table_name} " + 110 | "(#{cols.join(', ')}) " + 111 | "VALUES (#{vals.join(', ')})", 112 | "#{self.class.name} Create", 113 | self.class.primary_key, 114 | self.id 115 | ) 116 | @new_record = false 117 | return true 118 | end 119 | 120 | # Updates the associated record with values matching those of the instance attributes. 121 | def update_without_callbacks 122 | where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| 123 | "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" 124 | end 125 | where_clause = where_clause_terms.join(" AND ") 126 | connection.update( 127 | "UPDATE #{self.class.quoted_table_name} " + 128 | "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " + 129 | "WHERE #{where_clause}", 130 | "#{self.class.name} Update" 131 | ) 132 | return true 133 | end 134 | 135 | # Deletes the record in the database and freezes this instance to reflect that no changes should 136 | # be made (since they can't be persisted). 137 | def destroy_without_callbacks 138 | where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| 139 | "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" 140 | end 141 | where_clause = where_clause_terms.join(" AND ") 142 | unless new_record? 143 | connection.delete( 144 | "DELETE FROM #{self.class.quoted_table_name} " + 145 | "WHERE #{where_clause}", 146 | "#{self.class.name} Destroy" 147 | ) 148 | end 149 | freeze 150 | end 151 | end 152 | 153 | module CompositeClassMethods 154 | def primary_key; primary_keys; end 155 | def primary_key=(keys); primary_keys = keys; end 156 | 157 | def composite? 158 | true 159 | end 160 | 161 | #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)" 162 | #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3" 163 | def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')') 164 | many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep) 165 | end 166 | 167 | # Creates WHERE condition from list of composited ids 168 | # User.update_all({:role => 'admin'}, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> UPDATE admins SET admin.role='admin' WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2) 169 | # User.find(:all, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> SELECT * FROM admins WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2) 170 | def composite_where_clause(ids) 171 | if ids.is_a?(String) 172 | ids = [[ids]] 173 | elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1 174 | ids = [ids.to_composite_ids] 175 | end 176 | 177 | ids.map do |id_set| 178 | [primary_keys, id_set].transpose.map do |key, id| 179 | "#{table_name}.#{key.to_s}=#{sanitize(id)}" 180 | end.join(" AND ") 181 | end.join(") OR (") 182 | end 183 | 184 | # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise. 185 | # Example: 186 | # Person.exists?(5,7) 187 | def exists?(ids) 188 | if ids.is_a?(Array) && ids.first.is_a?(String) 189 | count(:conditions => ids) > 0 190 | else 191 | obj = find(ids) rescue false 192 | !obj.nil? and obj.is_a?(self) 193 | end 194 | end 195 | 196 | # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2) 197 | # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them 198 | # are deleted. 199 | def delete(*ids) 200 | unless ids.is_a?(Array); raise "*ids must be an Array"; end 201 | ids = [ids.to_composite_ids] if not ids.first.is_a?(Array) 202 | where_clause = ids.map do |id_set| 203 | [primary_keys, id_set].transpose.map do |key, id| 204 | "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}" 205 | end.join(" AND ") 206 | end.join(") OR (") 207 | delete_all([ "(#{where_clause})" ]) 208 | end 209 | 210 | # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered). 211 | # If an array of ids is provided, all of them are destroyed. 212 | def destroy(*ids) 213 | unless ids.is_a?(Array); raise "*ids must be an Array"; end 214 | if ids.first.is_a?(Array) 215 | ids = ids.map{|compids| compids.to_composite_ids} 216 | else 217 | ids = ids.to_composite_ids 218 | end 219 | ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy 220 | end 221 | 222 | # Returns an array of column objects for the table associated with this class. 223 | # Each column that matches to one of the primary keys has its 224 | # primary attribute set to true 225 | def columns 226 | unless @columns 227 | @columns = connection.columns(table_name, "#{name} Columns") 228 | @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)} 229 | end 230 | @columns 231 | end 232 | 233 | ## DEACTIVATED METHODS ## 234 | public 235 | # Lazy-set the sequence name to the connection's default. This method 236 | # is only ever called once since set_sequence_name overrides it. 237 | def sequence_name #:nodoc: 238 | raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS 239 | end 240 | 241 | def reset_sequence_name #:nodoc: 242 | raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS 243 | end 244 | 245 | def set_primary_key(value = nil, &block) 246 | raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS 247 | end 248 | 249 | private 250 | def find_one(id, options) 251 | raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS 252 | end 253 | 254 | def find_some(ids, options) 255 | raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS 256 | end 257 | 258 | def find_from_ids(ids, options) 259 | ids = ids.first if ids.last == nil 260 | conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] 261 | # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order) 262 | # if ids is list of lists, then each inner list must follow rule above 263 | if ids.first.is_a? String 264 | # find '2,1' -> ids = ['2,1'] 265 | # find '2,1;7,3' -> ids = ['2,1;7,3'] 266 | ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids} 267 | # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds 268 | end 269 | ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array) 270 | ids.each do |id_set| 271 | unless id_set.is_a?(Array) 272 | raise "Ids must be in an Array, instead received: #{id_set.inspect}" 273 | end 274 | unless id_set.length == primary_keys.length 275 | raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}" 276 | end 277 | end 278 | 279 | # Let keys = [:a, :b] 280 | # If ids = [[10, 50], [11, 51]], then :conditions => 281 | # "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))" 282 | 283 | conditions = ids.map do |id_set| 284 | [primary_keys, id_set].transpose.map do |key, id| 285 | col = columns_hash[key.to_s] 286 | val = quote_value(id, col) 287 | "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}" 288 | end.join(" AND ") 289 | end.join(") OR (") 290 | 291 | options.update :conditions => "(#{conditions})" 292 | 293 | result = find_every(options) 294 | 295 | if result.size == ids.size 296 | ids.size == 1 ? result[0] : result 297 | else 298 | raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}" 299 | end 300 | end 301 | end 302 | end 303 | end 304 | end 305 | 306 | 307 | module ActiveRecord 308 | ID_SEP = ',' 309 | ID_SET_SEP = ';' 310 | 311 | class Base 312 | # Allows +attr_name+ to be the list of primary_keys, and returns the id 313 | # of the object 314 | # e.g. @object[@object.class.primary_key] => [1,1] 315 | def [](attr_name) 316 | if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first 317 | attr_name = attr_name.split(ID_SEP) 318 | end 319 | attr_name.is_a?(Array) ? 320 | attr_name.map {|name| read_attribute(name)} : 321 | read_attribute(attr_name) 322 | end 323 | 324 | # Updates the attribute identified by attr_name with the specified +value+. 325 | # (Alias for the protected write_attribute method). 326 | def []=(attr_name, value) 327 | if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first 328 | attr_name = attr_name.split(ID_SEP) 329 | end 330 | 331 | if attr_name.is_a? Array 332 | value = value.split(ID_SEP) if value.is_a? String 333 | unless value.length == attr_name.length 334 | raise "Number of attr_names and values do not match" 335 | end 336 | #breakpoint 337 | [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)} 338 | else 339 | write_attribute(attr_name, value) 340 | end 341 | end 342 | end 343 | end 344 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/calculations.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | module ActiveRecord 3 | module Calculations 4 | def self.append_features(base) 5 | super 6 | base.send(:extend, ClassMethods) 7 | end 8 | 9 | module ClassMethods 10 | def construct_calculation_sql(operation, column_name, options) #:nodoc: 11 | operation = operation.to_s.downcase 12 | options = options.symbolize_keys 13 | 14 | scope = scope(:find) 15 | merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) 16 | aggregate_alias = column_alias_for(operation, column_name) 17 | use_workaround = !connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count' 18 | join_dependency = nil 19 | 20 | if merged_includes.any? && operation.to_s.downcase == 'count' 21 | options[:distinct] = true 22 | use_workaround = !connection.supports_count_distinct? 23 | column_name = options[:select] || primary_key.map{ |part| "#{quoted_table_name}.#{connection.quote_column_name(part)}"}.join(',') 24 | end 25 | 26 | sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}" 27 | 28 | # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. 29 | sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround 30 | 31 | sql << ", #{connection.quote_column_name(options[:group_field])} AS #{options[:group_alias]}" if options[:group] 32 | sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround 33 | sql << " FROM #{quoted_table_name} " 34 | if merged_includes.any? 35 | join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins]) 36 | sql << join_dependency.join_associations.collect{|join| join.association_join }.join 37 | end 38 | 39 | add_joins!(sql, options[:joins], scope) 40 | add_conditions!(sql, options[:conditions], scope) 41 | add_limited_ids_condition!(sql, options, join_dependency) if \ 42 | join_dependency && 43 | !using_limitable_reflections?(join_dependency.reflections) && 44 | ((scope && scope[:limit]) || options[:limit]) 45 | 46 | if options[:group] 47 | group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field 48 | sql << " GROUP BY #{connection.quote_column_name(options[group_key])} " 49 | end 50 | 51 | if options[:group] && options[:having] 52 | # FrontBase requires identifiers in the HAVING clause and chokes on function calls 53 | if connection.adapter_name == 'FrontBase' 54 | options[:having].downcase! 55 | options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) 56 | end 57 | 58 | sql << " HAVING #{options[:having]} " 59 | end 60 | 61 | sql << " ORDER BY #{options[:order]} " if options[:order] 62 | add_limit!(sql, options, scope) 63 | sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql 64 | sql 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/composite_arrays.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | ID_SEP = ',' 3 | ID_SET_SEP = ';' 4 | 5 | module ArrayExtension 6 | def to_composite_keys 7 | CompositeKeys.new(self) 8 | end 9 | 10 | def to_composite_ids 11 | CompositeIds.new(self) 12 | end 13 | end 14 | 15 | class CompositeArray < Array 16 | def to_s 17 | join(ID_SEP) 18 | end 19 | end 20 | 21 | class CompositeKeys < CompositeArray 22 | 23 | end 24 | 25 | class CompositeIds < CompositeArray 26 | 27 | end 28 | end 29 | 30 | Array.send(:include, CompositePrimaryKeys::ArrayExtension) 31 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/connection_adapters/abstract_adapter.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module ConnectionAdapters # :nodoc: 3 | class AbstractAdapter 4 | def concat(*columns) 5 | "CONCAT(#{columns.join(',')})" 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module ConnectionAdapters 3 | class IBM_DBAdapter < AbstractAdapter 4 | 5 | # This mightn't be in Core, but count(distinct x,y) doesn't work for me 6 | def supports_count_distinct? #:nodoc: 7 | false 8 | end 9 | 10 | alias_method :quote_original, :quote 11 | def quote(value, column = nil) 12 | if value.kind_of?(String) && column && [:integer, :float].include?(column.type) 13 | value = column.type == :integer ? value.to_i : value.to_f 14 | value.to_s 15 | else 16 | quote_original(value, column) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module ConnectionAdapters 3 | class OracleAdapter < AbstractAdapter 4 | 5 | # This mightn't be in Core, but count(distinct x,y) doesn't work for me 6 | def supports_count_distinct? #:nodoc: 7 | false 8 | end 9 | 10 | def concat(*columns) 11 | "(#{columns.join('||')})" 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/connection_adapters/oracle_enhanced_adapter.rb: -------------------------------------------------------------------------------- 1 | # Added to OracleEnhancedAdapter version 1.1.4 2 | # 3 | # module ActiveRecord 4 | # module ConnectionAdapters 5 | # class OracleEnhancedAdapter < AbstractAdapter 6 | # 7 | # # This mightn't be in Core, but count(distinct x,y) doesn't work for me 8 | # def supports_count_distinct? #:nodoc: 9 | # false 10 | # end 11 | # 12 | # def concat(*columns) 13 | # "(#{columns.join('||')})" 14 | # end 15 | # end 16 | # end 17 | # end -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module ConnectionAdapters 3 | class PostgreSQLAdapter < AbstractAdapter 4 | 5 | # This mightn't be in Core, but count(distinct x,y) doesn't work for me 6 | def supports_count_distinct? #:nodoc: 7 | false 8 | end 9 | 10 | def concat(*columns) 11 | columns = columns.map { |c| "CAST(#{c} AS varchar)" } 12 | "(#{columns.join('||')})" 13 | end 14 | 15 | # Executes an INSERT query and returns the new record's ID 16 | def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) 17 | # Extract the table from the insert sql. Yuck. 18 | table = sql.split(" ", 4)[2].gsub('"', '') 19 | 20 | # Try an insert with 'returning id' if available (PG >= 8.2) 21 | if supports_insert_with_returning? 22 | pk, sequence_name = *pk_and_sequence_for(table) unless pk 23 | if pk 24 | quoted_pk = if pk.is_a?(Array) 25 | pk.map { |col| quote_column_name(col) }.join(ID_SEP) 26 | else 27 | quote_column_name(pk) 28 | end 29 | id = select_value("#{sql} RETURNING #{quoted_pk}") 30 | clear_query_cache 31 | return id 32 | end 33 | end 34 | 35 | # Otherwise, insert then grab last_insert_id. 36 | if insert_id = super 37 | insert_id 38 | else 39 | # If neither pk nor sequence name is given, look them up. 40 | unless pk || sequence_name 41 | pk, sequence_name = *pk_and_sequence_for(table) 42 | end 43 | 44 | # If a pk is given, fallback to default sequence name. 45 | # Don't fetch last insert id for a table without a pk. 46 | if pk && sequence_name ||= default_sequence_name(table, pk) 47 | last_insert_id(table, sequence_name) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/sqlite_adapter' 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters #:nodoc: 5 | class SQLite3Adapter < SQLiteAdapter # :nodoc: 6 | def supports_count_distinct? #:nodoc: 7 | false 8 | end 9 | 10 | def concat(*columns) 11 | "(#{columns.join('||')})" 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/fixtures.rb: -------------------------------------------------------------------------------- 1 | class Fixture #:nodoc: 2 | def [](key) 3 | if key.is_a? Array 4 | return key.map { |a_key| self[a_key.to_s] }.to_composite_ids.to_s 5 | end 6 | @fixture[key] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/migration.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::ConnectionAdapters::ColumnDefinition.send(:alias_method, :to_s_without_composite_keys, :to_s) 2 | 3 | ActiveRecord::ConnectionAdapters::ColumnDefinition.class_eval <<-'EOF' 4 | def to_s 5 | if name.is_a? Array 6 | "PRIMARY KEY (#{name.join(',')})" 7 | else 8 | to_s_without_composite_keys 9 | end 10 | end 11 | EOF 12 | 13 | ActiveRecord::ConnectionAdapters::TableDefinition.class_eval <<-'EOF' 14 | def [](name) 15 | @columns.find { |column| 16 | !column.name.is_a?(Array) && column.name.to_s == name.to_s 17 | } 18 | end 19 | EOF 20 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/reflection.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Reflection 3 | class AssociationReflection 4 | def primary_key_name 5 | return @primary_key_name if @primary_key_name 6 | case 7 | when macro == :belongs_to 8 | @primary_key_name = options[:foreign_key] || class_name.foreign_key 9 | when options[:as] 10 | @primary_key_name = options[:foreign_key] || "#{options[:as]}_id" 11 | else 12 | @primary_key_name = options[:foreign_key] || active_record.name.foreign_key 13 | end 14 | @primary_key_name = @primary_key_name.to_composite_keys.to_s if @primary_key_name.is_a? Array 15 | @primary_key_name 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/validations/uniqueness.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | module ActiveRecord 3 | module Validations 4 | module Uniqueness 5 | module ClassMethods 6 | def validates_uniqueness_of(*attr_names) 7 | configuration = { :case_sensitive => true } 8 | configuration.update(attr_names.extract_options!) 9 | 10 | validates_each(attr_names,configuration) do |record, attr_name, value| 11 | # The check for an existing value should be run from a class that 12 | # isn't abstract. This means working down from the current class 13 | # (self), to the first non-abstract class. Since classes don't know 14 | # their subclasses, we have to build the hierarchy between self and 15 | # the record's class. 16 | class_hierarchy = [record.class] 17 | while class_hierarchy.first != self 18 | class_hierarchy.insert(0, class_hierarchy.first.superclass) 19 | end 20 | 21 | # Now we can work our way down the tree to the first non-abstract 22 | # class (which has a database table to query from). 23 | finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } 24 | 25 | column = finder_class.columns_hash[attr_name.to_s] 26 | 27 | if value.nil? 28 | comparison_operator = "IS ?" 29 | elsif column.text? 30 | comparison_operator = "#{connection.case_sensitive_equality_operator} ?" 31 | value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s 32 | else 33 | comparison_operator = "= ?" 34 | end 35 | 36 | sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" 37 | 38 | if value.nil? || (configuration[:case_sensitive] || !column.text?) 39 | condition_sql = "#{sql_attribute} #{comparison_operator}" 40 | condition_params = [value] 41 | else 42 | condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" 43 | condition_params = [value.mb_chars.downcase] 44 | end 45 | 46 | if scope = configuration[:scope] 47 | Array(scope).map do |scope_item| 48 | scope_value = record.send(scope_item) 49 | condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) 50 | condition_params << scope_value 51 | end 52 | end 53 | 54 | unless record.new_record? 55 | if record.class.composite? 56 | record.class.primary_keys.each do |key| 57 | condition_sql << " AND #{record.class.quoted_table_name}.#{key} <> ?" 58 | condition_params << record.send(key) 59 | end 60 | else 61 | condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" 62 | condition_params << record.send(:id) 63 | end 64 | end 65 | 66 | finder_class.with_exclusive_scope do 67 | if finder_class.exists?([condition_sql, *condition_params]) 68 | record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /composite_primary_keys/lib/composite_primary_keys/version.rb: -------------------------------------------------------------------------------- 1 | module CompositePrimaryKeys 2 | module VERSION #:nodoc: 3 | MAJOR = 2 4 | MINOR = 3 5 | TINY = 5 6 | PATCH = 1 7 | STRING = [MAJOR, MINOR, TINY, PATCH].join('.') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /composite_primary_keys/loader.rb: -------------------------------------------------------------------------------- 1 | # Load local config files in /local 2 | begin 3 | local_file_supported = Dir[File.join(PROJECT_ROOT, 'local/*.sample')].map { |path| File.basename(path).sub(".sample","") } 4 | local_file_supported.each do |file| 5 | require "local/#{file}" 6 | end 7 | rescue LoadError 8 | puts <<-EOS 9 | This Gem supports local developer extensions in local/ folder. 10 | Supported files: 11 | #{local_file_supported.map { |f| "local/#{f}"}.join(', ')} 12 | 13 | Setup default sample files: 14 | rake local:setup 15 | 16 | Current warning: #{$!} 17 | 18 | EOS 19 | end 20 | 21 | 22 | # Now load Rake tasks from /tasks 23 | rakefiles = Dir[File.join(File.dirname(__FILE__), "tasks/**/*.rake")] 24 | rakefiles.each { |rakefile| load File.expand_path(rakefile) } 25 | -------------------------------------------------------------------------------- /composite_primary_keys/local/database_connections.rb.sample: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | ENV['cpk_adapters'] = { 4 | "mysql" => { 5 | :adapter => "mysql", 6 | :username => "root", 7 | }, 8 | 9 | "sqlite3" => { 10 | :adapter => "sqlite3" 11 | } 12 | }.to_yaml -------------------------------------------------------------------------------- /composite_primary_keys/local/paths.rb.sample: -------------------------------------------------------------------------------- 1 | # location of folder containing activerecord, railties, etc folders for each Rails gem 2 | ENV['EDGE_RAILS_DIR'] ||= "/path/to/copy/of/edge/rails" 3 | -------------------------------------------------------------------------------- /composite_primary_keys/local/tasks.rb.sample: -------------------------------------------------------------------------------- 1 | # This file loaded into Rakefile 2 | # Place any extra development tasks you want here -------------------------------------------------------------------------------- /composite_primary_keys/scripts/console.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # if run as script, load the file as library while starting irb 5 | # 6 | if __FILE__ == $0 7 | irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' 8 | ENV['ADAPTER'] = ARGV[0] 9 | exec "#{irb} -f -r #{$0} --simple-prompt" 10 | end 11 | 12 | # 13 | # check if the given adapter is supported (default: mysql) 14 | # 15 | adapters = %w[mysql sqlite oracle oracle_enhanced postgresql ibm_db] 16 | adapter = ENV['ADAPTER'] || 'mysql' 17 | unless adapters.include? adapter 18 | puts "Usage: #{__FILE__} " 19 | puts '' 20 | puts 'Adapters: ' 21 | puts adapters.map{ |adapter| " #{adapter}" }.join("\n") 22 | exit 1 23 | end 24 | 25 | # 26 | # load all necessary libraries 27 | # 28 | require 'rubygems' 29 | require 'local/database_connections' 30 | 31 | $LOAD_PATH.unshift 'lib' 32 | 33 | begin 34 | require 'local/paths' 35 | $LOAD_PATH.unshift "#{ENV['EDGE_RAILS_DIR']}/activerecord/lib" if ENV['EDGE_RAILS_DIR'] 36 | $LOAD_PATH.unshift "#{ENV['EDGE_RAILS_DIR']}/activesupport/lib" if ENV['EDGE_RAILS_DIR'] 37 | rescue 38 | end 39 | 40 | require 'active_support' 41 | require 'active_record' 42 | 43 | require "test/connections/native_#{adapter}/connection" 44 | require 'composite_primary_keys' 45 | 46 | PROJECT_ROOT = File.join(File.dirname(__FILE__), '..') 47 | Dir[File.join(PROJECT_ROOT,'test/fixtures/*.rb')].each { |model| require model } 48 | 49 | -------------------------------------------------------------------------------- /composite_primary_keys/scripts/txt2html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'redcloth' 5 | require 'syntax/convertors/html' 6 | require 'erb' 7 | require File.dirname(__FILE__) + '/../lib/composite_primary_keys/version.rb' 8 | 9 | version = CompositePrimaryKeys::VERSION::STRING 10 | download = 'http://rubyforge.org/projects/compositekeys' 11 | 12 | class Fixnum 13 | def ordinal 14 | # teens 15 | return 'th' if (10..19).include?(self % 100) 16 | # others 17 | case self % 10 18 | when 1: return 'st' 19 | when 2: return 'nd' 20 | when 3: return 'rd' 21 | else return 'th' 22 | end 23 | end 24 | end 25 | 26 | class Time 27 | def pretty 28 | return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}" 29 | end 30 | end 31 | 32 | def convert_syntax(syntax, source) 33 | return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^
|
$!,'') 34 | end 35 | 36 | if ARGV.length >= 1 37 | src, template = ARGV 38 | template ||= File.dirname(__FILE__) + '/../website/template.rhtml' 39 | 40 | else 41 | puts("Usage: #{File.split($0).last} source.txt [template.rhtml] > output.html") 42 | exit! 43 | end 44 | 45 | template = ERB.new(File.open(template).read) 46 | 47 | title = nil 48 | body = nil 49 | File.open(src) do |fsrc| 50 | title_text = fsrc.readline 51 | body_text = fsrc.read 52 | syntax_items = [] 53 | body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)!m){ 54 | ident = syntax_items.length 55 | element, syntax, source = $1, $2, $3 56 | syntax_items << "<#{element} class=\"syntax\">#{convert_syntax(syntax, source)}" 57 | "syntax-temp-#{ident}" 58 | } 59 | title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip 60 | body = RedCloth.new(body_text).to_html 61 | body.gsub!(%r!(?:
)?syntax-temp-(\d+)(?:
)?!){ syntax_items[$1.to_i] } 62 | end 63 | stat = File.stat(src) 64 | created = stat.ctime 65 | modified = stat.mtime 66 | 67 | $stdout << template.result(binding) 68 | -------------------------------------------------------------------------------- /composite_primary_keys/scripts/txt2js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'redcloth' 5 | require 'syntax/convertors/html' 6 | require 'erb' 7 | require 'active_support' 8 | require File.dirname(__FILE__) + '/../lib/composite_primary_keys/version.rb' 9 | 10 | version = CompositePrimaryKeys::VERSION::STRING 11 | download = 'http://rubyforge.org/projects/compositekeys' 12 | 13 | class Fixnum 14 | def ordinal 15 | # teens 16 | return 'th' if (10..19).include?(self % 100) 17 | # others 18 | case self % 10 19 | when 1: return 'st' 20 | when 2: return 'nd' 21 | when 3: return 'rd' 22 | else return 'th' 23 | end 24 | end 25 | end 26 | 27 | class Time 28 | def pretty 29 | return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}" 30 | end 31 | end 32 | 33 | def convert_syntax(syntax, source) 34 | return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^
|
$!,'') 35 | end 36 | 37 | if ARGV.length >= 1 38 | src, template = ARGV 39 | template ||= File.dirname(__FILE__) + '/../website/template.js' 40 | else 41 | puts("Usage: #{File.split($0).last} source.txt [template.js] > output.html") 42 | exit! 43 | end 44 | 45 | template = ERB.new(File.open(template).read) 46 | 47 | title = nil 48 | body = nil 49 | File.open(src) do |fsrc| 50 | title_text = fsrc.readline 51 | body_text = fsrc.read 52 | title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip 53 | body = RedCloth.new(body_text) 54 | end 55 | stat = File.stat(src) 56 | created = stat.ctime 57 | modified = stat.mtime 58 | 59 | $stdout << template.result(binding) 60 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/activerecord_selection.rake: -------------------------------------------------------------------------------- 1 | namespace :ar do 2 | desc 'Pre-load edge rails ActiveRecord' 3 | task :edge do 4 | unless path = ENV['EDGE_RAILS_DIR'] || ENV['EDGE_RAILS'] 5 | puts <<-EOS 6 | 7 | Need to define env var EDGE_RAILS_DIR or EDGE_RAILS- root of edge rails on your machine. 8 | i) Get copy of Edge Rails - http://dev.rubyonrails.org 9 | ii) Set EDGE_RAILS_DIR to this folder in local/paths.rb - see local/paths.rb.sample for example 10 | or 11 | a) Set folder from environment or command line (rake ar:edge EDGE_RAILS_DIR=/path/to/rails) 12 | 13 | EOS 14 | exit 15 | end 16 | 17 | ENV['AR_LOAD_PATH'] = File.join(path, "activerecord/lib") 18 | end 19 | 20 | desc 'Pre-load ActiveRecord using VERSION=X.Y.Z, instead of latest' 21 | task :set do 22 | unless version = ENV['VERSION'] 23 | puts <<-EOS 24 | Usage: rake ar:get_version VERSION=1.15.3 25 | Specify the version number with VERSION=X.Y.Z; and make sure you have that activerecord gem version installed. 26 | 27 | EOS 28 | end 29 | version = nil if version == "" || version == [] 30 | begin 31 | version ? gem('activerecord', version) : gem('activerecord') 32 | require 'active_record' 33 | ENV['AR_LOAD_PATH'] = $:.reverse.find { |path| /activerecord/ =~ path } 34 | rescue LoadError 35 | puts <<-EOS 36 | Missing: Cannot find activerecord #{version} installed. 37 | Install: gem install activerecord -v #{version} 38 | 39 | EOS 40 | exit 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /composite_primary_keys/tasks/databases.rake: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | # UNTESTED - firebird sqlserver sqlserver_odbc db2 sybase openbase 4 | for adapter in %w( mysql sqlite oracle oracle_enhanced postgresql ibm_db ) 5 | Rake::TestTask.new("test_#{adapter}") { |t| 6 | t.libs << "test" << "test/connections/native_#{adapter}" 7 | t.pattern = "test/test_*.rb" 8 | t.verbose = true 9 | } 10 | end 11 | 12 | SCHEMA_PATH = File.join(PROJECT_ROOT, *%w(test fixtures db_definitions)) 13 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/databases/mysql.rake: -------------------------------------------------------------------------------- 1 | namespace :mysql do 2 | desc 'Build the MySQL test databases' 3 | task :build_databases => :load_connection do 4 | puts File.join(SCHEMA_PATH, 'mysql.sql') 5 | options_str = ENV['cpk_adapter_options_str'] 6 | # creates something like "-u#{username} -p#{password} -S#{socket}" 7 | sh %{ mysqladmin #{options_str} create "#{GEM_NAME}_unittest" } 8 | sh %{ mysql #{options_str} "#{GEM_NAME}_unittest" < #{File.join(SCHEMA_PATH, 'mysql.sql')} } 9 | end 10 | 11 | desc 'Drop the MySQL test databases' 12 | task :drop_databases => :load_connection do 13 | options_str = ENV['cpk_adapter_options_str'] 14 | sh %{ mysqladmin #{options_str} -f drop "#{GEM_NAME}_unittest" } 15 | end 16 | 17 | desc 'Rebuild the MySQL test databases' 18 | task :rebuild_databases => [:drop_databases, :build_databases] 19 | 20 | task :load_connection do 21 | require File.join(PROJECT_ROOT, %w[lib adapter_helper mysql]) 22 | spec = AdapterHelper::MySQL.load_connection_from_env 23 | options = {} 24 | options['u'] = spec[:username] if spec[:username] 25 | options['p'] = spec[:password] if spec[:password] 26 | options['S'] = spec[:sock] if spec[:sock] 27 | options_str = options.map { |key, value| "-#{key}#{value}" }.join(" ") 28 | ENV['cpk_adapter_options_str'] = options_str 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/databases/oracle.rake: -------------------------------------------------------------------------------- 1 | namespace :oracle do 2 | desc 'Build the Oracle test databases' 3 | task :build_databases => :load_connection do 4 | puts File.join(SCHEMA_PATH, 'oracle.sql') 5 | options_str = ENV['cpk_adapter_options_str'] 6 | sh %( sqlplus #{options_str} < #{File.join(SCHEMA_PATH, 'oracle.sql')} ) 7 | end 8 | 9 | desc 'Drop the Oracle test databases' 10 | task :drop_databases => :load_connection do 11 | puts File.join(SCHEMA_PATH, 'oracle.drop.sql') 12 | options_str = ENV['cpk_adapter_options_str'] 13 | sh %( sqlplus #{options_str} < #{File.join(SCHEMA_PATH, 'oracle.drop.sql')} ) 14 | end 15 | 16 | desc 'Rebuild the Oracle test databases' 17 | task :rebuild_databases => [:drop_databases, :build_databases] 18 | 19 | task :load_connection do 20 | require File.join(PROJECT_ROOT, %w[lib adapter_helper oracle]) 21 | spec = AdapterHelper::Oracle.load_connection_from_env 22 | ENV['cpk_adapter_options_str'] = "#{spec[:username]}/#{spec[:password]}@#{spec[:host]}" 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/databases/postgresql.rake: -------------------------------------------------------------------------------- 1 | namespace :postgresql do 2 | desc 'Build the PostgreSQL test databases' 3 | task :build_databases => :load_connection do 4 | sh %{ createdb "#{GEM_NAME}_unittest" } 5 | sh %{ psql "#{GEM_NAME}_unittest" -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} } 6 | end 7 | 8 | desc 'Drop the PostgreSQL test databases' 9 | task :drop_databases => :load_connection do 10 | sh %{ dropdb "#{GEM_NAME}_unittest" } 11 | end 12 | 13 | desc 'Rebuild the PostgreSQL test databases' 14 | task :rebuild_databases => [:drop_databases, :build_databases] 15 | 16 | task :load_connection do 17 | require File.join(PROJECT_ROOT, %w[lib adapter_helper postgresql]) 18 | spec = AdapterHelper::Postgresql.load_connection_from_env 19 | options = {} 20 | options['u'] = spec[:username] if spec[:username] 21 | options['p'] = spec[:password] if spec[:password] 22 | options_str = options.map { |key, value| "-#{key}#{value}" }.join(" ") 23 | ENV['cpk_adapter_options_str'] = options_str 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/databases/sqlite3.rake: -------------------------------------------------------------------------------- 1 | namespace :sqlite3 do 2 | desc 'Build the sqlite test databases' 3 | task :build_databases => :load_connection do 4 | file = File.join(SCHEMA_PATH, 'sqlite.sql') 5 | dbfile = File.join(PROJECT_ROOT, ENV['cpk_adapter_options_str']) 6 | cmd = "mkdir -p #{File.dirname(dbfile)}" 7 | puts cmd 8 | sh %{ #{cmd} } 9 | cmd = "sqlite3 #{dbfile} < #{file}" 10 | puts cmd 11 | sh %{ #{cmd} } 12 | end 13 | 14 | desc 'Drop the sqlite test databases' 15 | task :drop_databases => :load_connection do 16 | dbfile = ENV['cpk_adapter_options_str'] 17 | sh %{ rm -f #{dbfile} } 18 | end 19 | 20 | desc 'Rebuild the sqlite test databases' 21 | task :rebuild_databases => [:drop_databases, :build_databases] 22 | 23 | task :load_connection do 24 | require File.join(PROJECT_ROOT, %w[lib adapter_helper sqlite3]) 25 | spec = AdapterHelper::Sqlite3.load_connection_from_env 26 | ENV['cpk_adapter_options_str'] = spec[:dbfile] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/deployment.rake: -------------------------------------------------------------------------------- 1 | desc 'Release the website and new gem version' 2 | task :deploy => [:check_version, :website, :release] do 3 | puts "Remember to create SVN tag:" 4 | puts "svn copy svn+ssh://#{RUBYFORGE_USERNAME}@rubyforge.org/var/svn/#{PATH}/trunk " + 5 | "svn+ssh://#{RUBYFORGE_USERNAME}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} " 6 | puts "Suggested comment:" 7 | puts "Tagging release #{CHANGES}" 8 | end 9 | 10 | desc 'Runs tasks website_generate and install_gem as a local deployment of the gem' 11 | task :local_deploy => [:website_generate, :install_gem] 12 | 13 | task :check_version do 14 | unless ENV['VERSION'] 15 | puts 'Must pass a VERSION=x.y.z release version' 16 | exit 17 | end 18 | unless ENV['VERSION'] == VERS 19 | puts "Please update your version.rb to match the release version, currently #{VERS}" 20 | exit 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /composite_primary_keys/tasks/local_setup.rake: -------------------------------------------------------------------------------- 1 | namespace :local do 2 | desc 'Copies over the same local files ready for editing' 3 | task :setup do 4 | sample_files = Dir[File.join(PROJECT_ROOT, "local/*.rb.sample")] 5 | sample_files.each do |sample_file| 6 | file = sample_file.sub(".sample","") 7 | unless File.exists?(file) 8 | puts "Copying #{sample_file} -> #{file}" 9 | sh %{ cp #{sample_file} #{file} } 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /composite_primary_keys/tasks/website.rake: -------------------------------------------------------------------------------- 1 | desc 'Generate website files' 2 | task :website_generate do 3 | sh %{ ruby scripts/txt2html website/index.txt > website/index.html } 4 | sh %{ ruby scripts/txt2js website/version.txt > website/version.js } 5 | sh %{ ruby scripts/txt2js website/version-raw.txt > website/version-raw.js } 6 | end 7 | 8 | desc 'Upload website files to rubyforge' 9 | task :website_upload do 10 | config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml"))) 11 | host = "#{config["username"]}@rubyforge.org" 12 | remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/" 13 | local_dir = 'website' 14 | sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}} 15 | end 16 | 17 | desc 'Generate and upload website files' 18 | task :website => [:website_generate, :website_upload, :publish_docs] 19 | -------------------------------------------------------------------------------- /composite_primary_keys/test/README_tests.txt: -------------------------------------------------------------------------------- 1 | = Composite Primary Keys - Testing Readme 2 | 3 | == Testing an adapter 4 | 5 | There are tests available for the following adapters: 6 | 7 | * ibmdb 8 | * mysql 9 | * oracle 10 | * postgresql 11 | * sqlite 12 | 13 | To run the tests for on of the adapters, follow these steps (using mysql in the example): 14 | 15 | * rake -T | grep mysql 16 | 17 | rake mysql:build_databases # Build the MySQL test databases 18 | rake mysql:drop_databases # Drop the MySQL test databases 19 | rake mysql:rebuild_databases # Rebuild the MySQL test databases 20 | rake test_mysql # Run tests for test_mysql 21 | 22 | * rake mysql:build_databases 23 | * rake test_mysql 24 | 25 | == Testing against different ActiveRecord versions (or Edge Rails) 26 | 27 | ActiveRecord is a RubyGem within Rails, and is constantly being improved/changed on 28 | its repository (http://dev.rubyonrails.org). These changes may create errors for the CPK 29 | gem. So, we need a way to test CPK against Edge Rails, as well as officially released RubyGems. 30 | 31 | The default test (as above) uses the latest RubyGem in your cache. 32 | 33 | You can select an older RubyGem version by running the following: 34 | 35 | * rake ar:set VERSION=1.14.4 test_mysql 36 | 37 | == Edge Rails 38 | 39 | Before you can test CPK against Edge Rails, you must checkout a copy of edge rails somewhere (see http://dev.rubyonrails.org for for examples) 40 | 41 | * cd /path/to/gems 42 | * svn co http://svn.rubyonrails.org/rails/trunk rails 43 | 44 | Say the rails folder is /path/to/gems/rails 45 | 46 | Three ways to run CPK tests for Edge Rails: 47 | 48 | i) Run: 49 | 50 | EDGE_RAILS_DIR=/path/to/gems/rails rake ar:edge test_mysql 51 | 52 | ii) In your .profile, set the environment variable EDGE_RAILS_DIR=/path/to/gems/rails, 53 | and once you reload your profile, run: 54 | 55 | rake ar:edge test_mysql 56 | 57 | iii) Store the path in local/paths.rb. Run: 58 | 59 | cp local/paths.rb.sample local/paths.rb 60 | # Now set ENV['EDGE_RAILS_DIR']=/path/to/gems/rails 61 | rake ar:edge test_mysql 62 | 63 | These are all variations of the same theme: 64 | 65 | * Set the environment variable EDGE_RAILS_DIR to the path to Rails (which contains the activerecord/lib folder) 66 | * Run: rake ar:edge test_ 67 | 68 | -------------------------------------------------------------------------------- /composite_primary_keys/test/abstract_unit.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(ENV['AR_LOAD_PATH']) if ENV['AR_LOAD_PATH'] 2 | 3 | require 'test/unit' 4 | require 'hash_tricks' 5 | require 'rubygems' 6 | require 'active_record' 7 | require 'active_record/fixtures' 8 | begin 9 | require 'connection' 10 | rescue MissingSourceFile => e 11 | adapter = 'postgresql' #'sqlite' 12 | require "#{File.dirname(__FILE__)}/connections/native_#{adapter}/connection" 13 | end 14 | require 'composite_primary_keys' 15 | 16 | QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') unless Object.const_defined?(:QUOTED_TYPE) 17 | 18 | class ActiveSupport::TestCase #:nodoc: 19 | include ActiveRecord::TestFixtures 20 | 21 | self.fixture_path = File.dirname(__FILE__) + "/fixtures/" 22 | self.use_instantiated_fixtures = false 23 | self.use_transactional_fixtures = true 24 | 25 | def assert_date_from_db(expected, actual, message = nil) 26 | # SQL Server doesn't have a separate column type just for dates, 27 | # so the time is in the string and incorrectly formatted 28 | if current_adapter?(:SQLServerAdapter) 29 | assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00") 30 | elsif current_adapter?(:SybaseAdapter) 31 | assert_equal expected.to_s, actual.to_date.to_s, message 32 | else 33 | assert_equal expected.to_s, actual.to_s, message 34 | end 35 | end 36 | 37 | def assert_queries(num = 1) 38 | ActiveRecord::Base.connection.class.class_eval do 39 | self.query_count = 0 40 | alias_method :execute, :execute_with_query_counting 41 | end 42 | yield 43 | ensure 44 | ActiveRecord::Base.connection.class.class_eval do 45 | alias_method :execute, :execute_without_query_counting 46 | end 47 | assert_equal num, ActiveRecord::Base.connection.query_count, "#{ActiveRecord::Base.connection.query_count} instead of #{num} queries were executed." 48 | end 49 | 50 | def assert_no_queries(&block) 51 | assert_queries(0, &block) 52 | end 53 | 54 | cattr_accessor :classes 55 | protected 56 | 57 | def testing_with(&block) 58 | classes.keys.each do |@key_test| 59 | @klass_info = classes[@key_test] 60 | @klass, @primary_keys = @klass_info[:class], @klass_info[:primary_keys] 61 | order = @klass.primary_key.is_a?(String) ? @klass.primary_key : @klass.primary_key.join(',') 62 | @first = @klass.find(:first, :order => order) 63 | yield 64 | end 65 | end 66 | 67 | def first_id 68 | ids = (1..@primary_keys.length).map {|num| 1} 69 | composite? ? ids.to_composite_ids : ids.first 70 | end 71 | 72 | def first_id_str 73 | composite? ? first_id.join(CompositePrimaryKeys::ID_SEP) : first_id.to_s 74 | end 75 | 76 | def composite? 77 | @key_test != :single 78 | end 79 | end 80 | 81 | def current_adapter?(type) 82 | ActiveRecord::ConnectionAdapters.const_defined?(type) && 83 | ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters.const_get(type)) 84 | end 85 | 86 | ActiveRecord::Base.connection.class.class_eval do 87 | cattr_accessor :query_count 88 | alias_method :execute_without_query_counting, :execute 89 | def execute_with_query_counting(sql, name = nil) 90 | self.query_count += 1 91 | execute_without_query_counting(sql, name) 92 | end 93 | end 94 | 95 | #ActiveRecord::Base.logger = Logger.new(STDOUT) 96 | #ActiveRecord::Base.colorize_logging = false 97 | -------------------------------------------------------------------------------- /composite_primary_keys/test/connections/native_ibm_db/connection.rb: -------------------------------------------------------------------------------- 1 | print "Using IBM2 \n" 2 | require 'logger' 3 | 4 | gem 'ibm_db' 5 | require 'IBM_DB' 6 | 7 | RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase ibm_db ) 8 | 9 | 10 | ActiveRecord::Base.logger = Logger.new("debug.log") 11 | 12 | db1 = 'composite_primary_keys_unittest' 13 | 14 | connection_options = { 15 | :adapter => "ibm_db", 16 | :database => "ocdpdev", 17 | :username => "db2inst1", 18 | :password => "password", 19 | :host => '192.168.2.21' 20 | } 21 | 22 | ActiveRecord::Base.configurations = { db1 => connection_options } 23 | ActiveRecord::Base.establish_connection(connection_options) 24 | -------------------------------------------------------------------------------- /composite_primary_keys/test/connections/native_mysql/connection.rb: -------------------------------------------------------------------------------- 1 | print "Using native MySQL\n" 2 | require 'fileutils' 3 | require 'logger' 4 | require 'adapter_helper/mysql' 5 | 6 | log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log])) 7 | FileUtils.mkdir_p log_path 8 | puts "Logging to #{log_path}/debug.log" 9 | ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log") 10 | 11 | # Adapter config setup in locals/database_connections.rb 12 | connection_options = AdapterHelper::MySQL.load_connection_from_env 13 | ActiveRecord::Base.establish_connection(connection_options) 14 | -------------------------------------------------------------------------------- /composite_primary_keys/test/connections/native_oracle/connection.rb: -------------------------------------------------------------------------------- 1 | print "Using native Oracle\n" 2 | require 'fileutils' 3 | require 'logger' 4 | require 'adapter_helper/oracle' 5 | 6 | log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log])) 7 | FileUtils.mkdir_p log_path 8 | puts "Logging to #{log_path}/debug.log" 9 | ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log") 10 | 11 | # Adapter config setup in locals/database_connections.rb 12 | connection_options = AdapterHelper::Oracle.load_connection_from_env 13 | puts connection_options.inspect 14 | ActiveRecord::Base.establish_connection(connection_options) 15 | -------------------------------------------------------------------------------- /composite_primary_keys/test/connections/native_oracle_enhanced/connection.rb: -------------------------------------------------------------------------------- 1 | print "Using native Oracle Enhanced\n" 2 | require 'fileutils' 3 | require 'logger' 4 | require 'adapter_helper/oracle_enhanced' 5 | 6 | log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log])) 7 | FileUtils.mkdir_p log_path 8 | puts "Logging to #{log_path}/debug.log" 9 | ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log") 10 | ActiveRecord::Base.logger.level = Logger::DEBUG 11 | 12 | # Adapter config setup in locals/database_connections.rb 13 | connection_options = AdapterHelper::OracleEnhanced.load_connection_from_env 14 | puts connection_options.inspect 15 | ActiveRecord::Base.establish_connection(connection_options) 16 | 17 | # Change default options for Oracle Enhanced adapter 18 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true 19 | # Change NLS_DATE_FORMAT to non-default format to verify that all tests should pass 20 | ActiveRecord::Base.connection.execute %q{alter session set nls_date_format = 'DD-MON-YYYY HH24:MI:SS'} 21 | -------------------------------------------------------------------------------- /composite_primary_keys/test/connections/native_postgresql/connection.rb: -------------------------------------------------------------------------------- 1 | print "Using native Postgresql\n" 2 | require 'logger' 3 | require 'adapter_helper/postgresql' 4 | 5 | ActiveRecord::Base.logger = Logger.new("debug.log") 6 | 7 | # Adapter config setup in locals/database_connections.rb 8 | connection_options = AdapterHelper::Postgresql.load_connection_from_env 9 | ActiveRecord::Base.establish_connection(connection_options) 10 | -------------------------------------------------------------------------------- /composite_primary_keys/test/connections/native_sqlite/connection.rb: -------------------------------------------------------------------------------- 1 | print "Using native Sqlite3\n" 2 | require 'logger' 3 | require 'adapter_helper/sqlite3' 4 | 5 | ActiveRecord::Base.logger = Logger.new("debug.log") 6 | 7 | # Adapter config setup in locals/database_connections.rb 8 | connection_options = AdapterHelper::Sqlite3.load_connection_from_env 9 | ActiveRecord::Base.establish_connection(connection_options) 10 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ActiveRecord::Base 2 | has_many :readings 3 | has_many :users, :through => :readings 4 | end 5 | 6 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/article_group.rb: -------------------------------------------------------------------------------- 1 | class ArticleGroup < ActiveRecord::Base 2 | belongs_to :article 3 | belongs_to :group 4 | end 5 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/article_groups.yml: -------------------------------------------------------------------------------- 1 | cpk: 2 | article_id: 1 3 | group_id: 1 4 | 5 | transformers: 6 | article_id: 1 7 | group_id: 2 8 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/articles.yml: -------------------------------------------------------------------------------- 1 | first: 2 | id: 1 3 | name: Article One 4 | second: 5 | id: 2 6 | name: Article Two -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | set_primary_keys :id 3 | belongs_to :person, :polymorphic => true 4 | belongs_to :hack 5 | end 6 | 7 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | comment1: 2 | id: 1 3 | person_id: 1 4 | person_type: Employee 5 | 6 | comment2: 7 | id: 2 8 | person_id: 1 9 | person_type: User 10 | hack_id: andrew 11 | 12 | comment3: 13 | id: 3 14 | person_id: andrew 15 | person_type: Hack 16 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/db2-create-tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE reference_types ( 2 | reference_type_id integer NOT NULL generated by default as identity (start with 100, increment by 1, no cache), 3 | type_label varchar(50) default NULL, 4 | abbreviation varchar(50) default NULL, 5 | description varchar(50) default NULL, 6 | PRIMARY KEY (reference_type_id) 7 | ); 8 | 9 | CREATE TABLE reference_codes ( 10 | reference_type_id integer, 11 | reference_code integer NOT NULL, 12 | code_label varchar(50) default NULL, 13 | abbreviation varchar(50) default NULL, 14 | description varchar(50) default NULL, 15 | PRIMARY KEY (reference_type_id,reference_code) 16 | ); 17 | 18 | CREATE TABLE products ( 19 | id integer NOT NULL, 20 | name varchar(50) default NULL, 21 | PRIMARY KEY (id) 22 | ); 23 | 24 | CREATE TABLE tariffs ( 25 | tariff_id integer NOT NULL, 26 | start_date date NOT NULL, 27 | amount integer default NULL, 28 | PRIMARY KEY (tariff_id,start_date) 29 | ); 30 | 31 | CREATE TABLE product_tariffs ( 32 | product_id integer NOT NULL, 33 | tariff_id integer NOT NULL, 34 | tariff_start_date date NOT NULL, 35 | PRIMARY KEY (product_id,tariff_id,tariff_start_date) 36 | ); 37 | 38 | CREATE TABLE suburbs ( 39 | city_id integer NOT NULL, 40 | suburb_id integer NOT NULL, 41 | name varchar(50) NOT NULL, 42 | PRIMARY KEY (city_id,suburb_id) 43 | ); 44 | 45 | CREATE TABLE streets ( 46 | id integer NOT NULL , 47 | city_id integer NOT NULL, 48 | suburb_id integer NOT NULL, 49 | name varchar(50) NOT NULL, 50 | PRIMARY KEY (id) 51 | ); 52 | 53 | CREATE TABLE users ( 54 | id integer NOT NULL , 55 | name varchar(50) NOT NULL, 56 | PRIMARY KEY (id) 57 | ); 58 | 59 | CREATE TABLE articles ( 60 | id integer NOT NULL , 61 | name varchar(50) NOT NULL, 62 | PRIMARY KEY (id) 63 | ); 64 | 65 | CREATE TABLE readings ( 66 | id integer NOT NULL , 67 | user_id integer NOT NULL, 68 | article_id integer NOT NULL, 69 | rating integer NOT NULL, 70 | PRIMARY KEY (id) 71 | ); 72 | 73 | CREATE TABLE groups ( 74 | id integer NOT NULL , 75 | name varchar(50) NOT NULL, 76 | PRIMARY KEY (id) 77 | ); 78 | 79 | CREATE TABLE memberships ( 80 | user_id integer NOT NULL, 81 | group_id integer NOT NULL, 82 | PRIMARY KEY (user_id,group_id) 83 | ); 84 | 85 | CREATE TABLE membership_statuses ( 86 | id integer NOT NULL , 87 | user_id integer NOT NULL, 88 | group_id integer NOT NULL, 89 | status varchar(50) NOT NULL, 90 | PRIMARY KEY (id) 91 | ); 92 | 93 | create table kitchen_sinks ( 94 | id_1 integer not null, 95 | id_2 integer not null, 96 | a_date date, 97 | a_string varchar(100), 98 | primary key (id_1, id_2) 99 | ); 100 | 101 | create table restaurants ( 102 | franchise_id integer not null, 103 | store_id integer not null, 104 | name varchar(100), 105 | primary key (franchise_id, store_id) 106 | ); 107 | 108 | create table restaurants_suburbs ( 109 | franchise_id integer not null, 110 | store_id integer not null, 111 | city_id integer not null, 112 | suburb_id integer not null 113 | ); 114 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/db2-drop-tables.sql: -------------------------------------------------------------------------------- 1 | drop table MEMBERSHIPS; 2 | drop table REFERENCE_CODES; 3 | drop table TARIFFS; 4 | drop table ARTICLES; 5 | drop table GROUPS; 6 | drop table MEMBERSHIP_STATUSES; 7 | drop table READINGS; 8 | drop table REFERENCE_TYPES; 9 | drop table STREETS; 10 | drop table PRODUCTS; 11 | drop table USERS; 12 | drop table SUBURBS; 13 | drop table PRODUCT_TARIFFS; 14 | drop table KITCHEN_SINK; 15 | drop table RESTAURANTS; 16 | drop table RESTAURANTS_SUBURBS; 17 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/mysql.sql: -------------------------------------------------------------------------------- 1 | create table reference_types ( 2 | reference_type_id int(11) not null auto_increment, 3 | type_label varchar(50) default null, 4 | abbreviation varchar(50) default null, 5 | description varchar(50) default null, 6 | primary key (reference_type_id) 7 | ) type=InnoDB; 8 | 9 | create table reference_codes ( 10 | reference_type_id int(11), 11 | reference_code int(11) not null, 12 | code_label varchar(50) default null, 13 | abbreviation varchar(50) default null, 14 | description varchar(50) default null, 15 | primary key (reference_type_id, reference_code) 16 | ) type=InnoDB; 17 | 18 | create table products ( 19 | id int(11) not null auto_increment, 20 | name varchar(50) default null, 21 | primary key (id) 22 | ) type=InnoDB; 23 | 24 | create table tariffs ( 25 | tariff_id int(11) not null, 26 | start_date date not null, 27 | amount integer(11) default null, 28 | primary key (tariff_id, start_date) 29 | ) type=InnoDB; 30 | 31 | create table product_tariffs ( 32 | product_id int(11) not null, 33 | tariff_id int(11) not null, 34 | tariff_start_date date not null, 35 | primary key (product_id, tariff_id, tariff_start_date) 36 | ) type=InnoDB; 37 | 38 | create table suburbs ( 39 | city_id int(11) not null, 40 | suburb_id int(11) not null, 41 | name varchar(50) not null, 42 | primary key (city_id, suburb_id) 43 | ) type=InnoDB; 44 | 45 | create table streets ( 46 | id int(11) not null auto_increment, 47 | city_id int(11) not null, 48 | suburb_id int(11) not null, 49 | name varchar(50) not null, 50 | primary key (id) 51 | ) type=InnoDB; 52 | 53 | create table users ( 54 | id int(11) not null auto_increment, 55 | name varchar(50) not null, 56 | primary key (id) 57 | ) type=InnoDB; 58 | 59 | create table articles ( 60 | id int(11) not null auto_increment, 61 | name varchar(50) not null, 62 | primary key (id) 63 | ) type=InnoDB; 64 | 65 | create table readings ( 66 | id int(11) not null auto_increment, 67 | user_id int(11) not null, 68 | article_id int(11) not null, 69 | rating int(11) not null, 70 | primary key (id) 71 | ) type=InnoDB; 72 | 73 | create table groups ( 74 | id int(11) not null auto_increment, 75 | name varchar(50) not null, 76 | primary key (id) 77 | ) type=InnoDB; 78 | 79 | create table memberships ( 80 | user_id int(11) not null, 81 | group_id int(11) not null, 82 | primary key (user_id,group_id) 83 | ) type=InnoDB; 84 | 85 | create table membership_statuses ( 86 | id int(11) not null auto_increment, 87 | user_id int(11) not null, 88 | group_id int(11) not null, 89 | status varchar(50) not null, 90 | primary key (id) 91 | ) type=InnoDB; 92 | 93 | create table departments ( 94 | department_id int(11) not null, 95 | location_id int(11) not null, 96 | primary key (department_id, location_id) 97 | ) type=InnoDB; 98 | 99 | create table employees ( 100 | id int(11) not null auto_increment, 101 | department_id int(11) default null, 102 | location_id int(11) default null, 103 | primary key (id) 104 | ) type=InnoDB; 105 | 106 | create table comments ( 107 | id int(11) not null auto_increment, 108 | person_id varchar(100) default null, 109 | person_type varchar(100) default null, 110 | hack_id varchar(100) default null, 111 | primary key (id) 112 | ) type=InnoDB; 113 | 114 | create table hacks ( 115 | name varchar(50) not null, 116 | primary key (name) 117 | ) type=InnoDB; 118 | 119 | create table kitchen_sinks ( 120 | id_1 int(11) not null, 121 | id_2 int(11) not null, 122 | a_date date, 123 | a_string varchar(100), 124 | primary key (id_1, id_2) 125 | ) type=InnoDB; 126 | 127 | create table restaurants ( 128 | franchise_id int(11) not null, 129 | store_id int(11) not null, 130 | name varchar(100), 131 | primary key (franchise_id, store_id) 132 | ) type=InnoDB; 133 | 134 | create table restaurants_suburbs ( 135 | franchise_id int(11) not null, 136 | store_id int(11) not null, 137 | city_id int(11) not null, 138 | suburb_id int(11) not null 139 | ) type=InnoDB; 140 | 141 | create table dorms ( 142 | id int(11) not null auto_increment, 143 | primary key(id) 144 | ) type=InnoDB; 145 | 146 | create table rooms ( 147 | dorm_id int(11) not null, 148 | room_id int(11) not null, 149 | primary key (dorm_id, room_id) 150 | ) type=InnoDB; 151 | 152 | create table room_attributes ( 153 | id int(11) not null auto_increment, 154 | name varchar(50), 155 | primary key(id) 156 | ) type=InnoDB; 157 | 158 | create table room_attribute_assignments ( 159 | dorm_id int(11) not null, 160 | room_id int(11) not null, 161 | room_attribute_id int(11) not null 162 | ) type=InnoDB; 163 | 164 | create table students ( 165 | id int(11) not null auto_increment, 166 | primary key(id) 167 | ) type=InnoDB; 168 | 169 | create table room_assignments ( 170 | student_id int(11) not null, 171 | dorm_id int(11) not null, 172 | room_id int(11) not null 173 | ) type=InnoDB; 174 | 175 | create table seats ( 176 | flight_number int(11) not null, 177 | seat int(11) not null, 178 | customer int, 179 | primary key (flight_number, seat) 180 | ) type=InnoDB; 181 | 182 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/oracle.drop.sql: -------------------------------------------------------------------------------- 1 | drop table reference_types; 2 | drop sequence reference_types_seq; 3 | drop table reference_codes; 4 | drop table products; 5 | drop sequence products_seq; 6 | drop table tariffs; 7 | drop table product_tariffs; 8 | drop table suburbs; 9 | drop table streets; 10 | drop sequence streets_seq; 11 | drop table users; 12 | drop sequence users_seq; 13 | drop table articles; 14 | drop sequence articles_seq; 15 | drop table readings; 16 | drop sequence readings_seq; 17 | drop table groups; 18 | drop sequence groups_seq; 19 | drop table memberships; 20 | drop table membership_statuses; 21 | drop sequence membership_statuses_seq; 22 | drop table departments; 23 | drop table employees; 24 | drop sequence employees_seq; 25 | drop table comments; 26 | drop sequence comments_seq; 27 | drop table hacks; 28 | drop table kitchen_sinks; 29 | drop table restaurants; 30 | drop table restaurants_suburbs; 31 | drop table dorms; 32 | drop sequence dorms_seq; 33 | drop table rooms; 34 | drop table room_attributes; 35 | drop sequence room_attributes_seq; 36 | drop table room_attribute_assignments; 37 | drop table room_assignments; 38 | drop table students; 39 | drop sequence students_seq; 40 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/oracle.sql: -------------------------------------------------------------------------------- 1 | create sequence reference_types_seq start with 1000; 2 | 3 | create table reference_types ( 4 | reference_type_id number(11) primary key, 5 | type_label varchar2(50) default null, 6 | abbreviation varchar2(50) default null, 7 | description varchar2(50) default null 8 | ); 9 | 10 | create table reference_codes ( 11 | reference_type_id number(11), 12 | reference_code number(11), 13 | code_label varchar2(50) default null, 14 | abbreviation varchar2(50) default null, 15 | description varchar2(50) default null 16 | ); 17 | 18 | create sequence products_seq start with 1000; 19 | 20 | create table products ( 21 | id number(11) primary key, 22 | name varchar2(50) default null 23 | ); 24 | 25 | create table tariffs ( 26 | tariff_id number(11), 27 | start_date date, 28 | amount number(11) default null, 29 | constraint tariffs_pk primary key (tariff_id, start_date) 30 | ); 31 | 32 | create table product_tariffs ( 33 | product_id number(11), 34 | tariff_id number(11), 35 | tariff_start_date date, 36 | constraint product_tariffs_pk primary key (product_id, tariff_id, tariff_start_date) 37 | ); 38 | 39 | create table suburbs ( 40 | city_id number(11), 41 | suburb_id number(11), 42 | name varchar2(50) not null, 43 | constraint suburbs_pk primary key (city_id, suburb_id) 44 | ); 45 | 46 | create sequence streets_seq start with 1000; 47 | 48 | create table streets ( 49 | id number(11) primary key, 50 | city_id number(11) not null, 51 | suburb_id number(11) not null, 52 | name varchar2(50) not null 53 | ); 54 | 55 | create sequence users_seq start with 1000; 56 | 57 | create table users ( 58 | id number(11) primary key, 59 | name varchar2(50) not null 60 | ); 61 | 62 | create sequence articles_seq start with 1000; 63 | 64 | create table articles ( 65 | id number(11) primary key, 66 | name varchar2(50) not null 67 | ); 68 | 69 | create sequence readings_seq start with 1000; 70 | 71 | create table readings ( 72 | id number(11) primary key, 73 | user_id number(11) not null, 74 | article_id number(11) not null, 75 | rating number(11) not null 76 | ); 77 | 78 | create sequence groups_seq start with 1000; 79 | 80 | create table groups ( 81 | id number(11) primary key, 82 | name varchar2(50) not null 83 | ); 84 | 85 | create table memberships ( 86 | user_id number(11) not null, 87 | group_id number(11) not null, 88 | constraint memberships_pk primary key (user_id, group_id) 89 | ); 90 | 91 | create sequence membership_statuses_seq start with 1000; 92 | 93 | create table membership_statuses ( 94 | id number(11) primary key, 95 | user_id number(11) not null, 96 | group_id number(11) not null, 97 | status varchar2(50) not null 98 | ); 99 | 100 | create table departments ( 101 | department_id number(11) not null, 102 | location_id number(11) not null, 103 | constraint departments_pk primary key (department_id, location_id) 104 | ); 105 | 106 | create sequence employees_seq start with 1000; 107 | 108 | create table employees ( 109 | id number(11) not null primary key, 110 | department_id number(11) default null, 111 | location_id number(11) default null 112 | ); 113 | 114 | create sequence comments_seq start with 1000; 115 | 116 | create table comments ( 117 | id number(11) not null primary key, 118 | person_id varchar(100) default null, 119 | person_type varchar(100) default null, 120 | hack_id varchar(100) default null 121 | ); 122 | 123 | create table hacks ( 124 | name varchar(50) not null primary key 125 | ); 126 | 127 | create table kitchen_sinks ( 128 | id_1 number(11) not null, 129 | id_2 number(11) not null, 130 | a_date date, 131 | a_string varchar(100), 132 | constraint kitchen_sinks_pk primary key (id_1, id_2) 133 | ); 134 | 135 | create table restaurants ( 136 | franchise_id number(11) not null, 137 | store_id number(11) not null, 138 | name varchar(100), 139 | constraint restaurants_pk primary key (franchise_id, store_id) 140 | ); 141 | 142 | create table restaurants_suburbs ( 143 | franchise_id number(11) not null, 144 | store_id number(11) not null, 145 | city_id number(11) not null, 146 | suburb_id number(11) not null 147 | ); 148 | 149 | create sequence dorms_seq start with 1000; 150 | 151 | create table dorms ( 152 | id number(11) not null, 153 | constraint dorms_pk primary key (id) 154 | ); 155 | 156 | create table rooms ( 157 | dorm_id number(11) not null, 158 | room_id number(11) not null, 159 | constraint rooms_pk primary key (dorm_id, room_id) 160 | ); 161 | 162 | create sequence room_attributes_seq start with 1000; 163 | 164 | create table room_attributes ( 165 | id number(11) not null, 166 | name varchar(50), 167 | constraint room_attributes_pk primary key (id) 168 | ); 169 | 170 | create table room_attribute_assignments ( 171 | dorm_id number(11) not null, 172 | room_id number(11) not null, 173 | room_attribute_id number(11) not null 174 | ); 175 | 176 | create sequence students_seq start with 1000; 177 | 178 | create table students ( 179 | id number(11) not null, 180 | constraint students_pk primary key (id) 181 | ); 182 | 183 | create table room_assignments ( 184 | student_id number(11) not null, 185 | dorm_id number(11) not null, 186 | room_id number(11) not null 187 | ); 188 | 189 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/postgresql.sql: -------------------------------------------------------------------------------- 1 | create sequence public.reference_types_seq start 1000; 2 | 3 | create table reference_types ( 4 | reference_type_id int default nextval('public.reference_types_seq'), 5 | type_label varchar(50) default null, 6 | abbreviation varchar(50) default null, 7 | description varchar(50) default null, 8 | primary key (reference_type_id) 9 | ); 10 | 11 | create table reference_codes ( 12 | reference_type_id int, 13 | reference_code int not null, 14 | code_label varchar(50) default null, 15 | abbreviation varchar(50) default null, 16 | description varchar(50) default null 17 | ); 18 | 19 | create sequence public.products_seq start 1000; 20 | 21 | create table products ( 22 | id int not null default nextval('public.products_seq'), 23 | name varchar(50) default null, 24 | primary key (id) 25 | ); 26 | 27 | create table tariffs ( 28 | tariff_id int not null, 29 | start_date date not null, 30 | amount int default null, 31 | primary key (tariff_id, start_date) 32 | ); 33 | 34 | create table product_tariffs ( 35 | product_id int not null, 36 | tariff_id int not null, 37 | tariff_start_date date not null, 38 | primary key (product_id, tariff_id, tariff_start_date) 39 | ); 40 | 41 | create table suburbs ( 42 | city_id int not null, 43 | suburb_id int not null, 44 | name varchar(50) not null, 45 | primary key (city_id, suburb_id) 46 | ); 47 | 48 | create sequence public.streets_seq start 1000; 49 | 50 | create table streets ( 51 | id int not null default nextval('public.streets_seq'), 52 | city_id int not null, 53 | suburb_id int not null, 54 | name varchar(50) not null, 55 | primary key (id) 56 | ); 57 | 58 | create sequence public.users_seq start 1000; 59 | 60 | create table users ( 61 | id int not null default nextval('public.users_seq'), 62 | name varchar(50) not null, 63 | primary key (id) 64 | ); 65 | 66 | create sequence public.articles_seq start 1000; 67 | 68 | create table articles ( 69 | id int not null default nextval('public.articles_seq'), 70 | name varchar(50) not null, 71 | primary key (id) 72 | ); 73 | 74 | create sequence public.readings_seq start 1000; 75 | 76 | create table readings ( 77 | id int not null default nextval('public.readings_seq'), 78 | user_id int not null, 79 | article_id int not null, 80 | rating int not null, 81 | primary key (id) 82 | ); 83 | 84 | create sequence public.groups_seq start 1000; 85 | 86 | create table groups ( 87 | id int not null default nextval('public.groups_seq'), 88 | name varchar(50) not null, 89 | primary key (id) 90 | ); 91 | 92 | create table memberships ( 93 | user_id int not null, 94 | group_id int not null, 95 | primary key (user_id, group_id) 96 | ); 97 | 98 | create sequence public.membership_statuses_seq start 1000; 99 | 100 | create table membership_statuses ( 101 | id int not null default nextval('public.membership_statuses_seq'), 102 | user_id int not null, 103 | group_id int not null, 104 | status varchar(50) not null, 105 | primary key (id) 106 | ); 107 | 108 | create table departments ( 109 | department_id int not null, 110 | location_id int not null, 111 | primary key (department_id, location_id) 112 | ); 113 | 114 | create sequence public.employees_seq start 1000; 115 | 116 | create table employees ( 117 | id int not null default nextval('public.employees_seq'), 118 | department_id int default null, 119 | location_id int default null, 120 | primary key (id) 121 | ); 122 | 123 | create sequence public.comments_seq start 1000; 124 | 125 | create table comments ( 126 | id int not null default nextval('public.comments_seq'), 127 | person_id varchar(100) default null, 128 | person_type varchar(100) default null, 129 | hack_id varchar(100) default null, 130 | primary key (id) 131 | ); 132 | 133 | create table hacks ( 134 | name varchar(50) not null, 135 | primary key (name) 136 | ); 137 | 138 | create table kitchen_sinks ( 139 | id_1 int not null, 140 | id_2 int not null, 141 | a_date date, 142 | a_string varchar(100), 143 | primary key (id_1, id_2) 144 | ); 145 | 146 | create table restaurants ( 147 | franchise_id int not null, 148 | store_id int not null, 149 | name varchar(100), 150 | primary key (franchise_id, store_id) 151 | ); 152 | 153 | create table restaurants_suburbs ( 154 | franchise_id int not null, 155 | store_id int not null, 156 | city_id int not null, 157 | suburb_id int not null 158 | ); 159 | 160 | create sequence public.dorms_seq start 1000; 161 | 162 | create table dorms ( 163 | id int not null default nextval('public.dorms_seq'), 164 | primary key (id) 165 | ); 166 | 167 | create table rooms ( 168 | dorm_id int not null, 169 | room_id int not null, 170 | primary key (dorm_id, room_id) 171 | ); 172 | 173 | create sequence public.room_attributes_seq start 1000; 174 | 175 | create table room_attributes ( 176 | id int not null default nextval('public.room_attributes_seq'), 177 | name varchar(50), 178 | primary key (id) 179 | ); 180 | 181 | create table room_attribute_assignments ( 182 | dorm_id int not null, 183 | room_id int not null, 184 | room_attribute_id int not null 185 | ); 186 | 187 | create sequence public.students_seq start 1000; 188 | 189 | create table students ( 190 | id int not null default nextval('public.students_seq'), 191 | primary key (id) 192 | ); 193 | 194 | create table room_assignments ( 195 | student_id int not null, 196 | dorm_id int not null, 197 | room_id int not null 198 | ); 199 | 200 | create table seats ( 201 | flight_number int not null, 202 | seat int not null, 203 | customer int, 204 | primary key (flight_number, seat) 205 | ); 206 | 207 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/db_definitions/sqlite.sql: -------------------------------------------------------------------------------- 1 | create table reference_types ( 2 | reference_type_id integer primary key, 3 | type_label varchar(50) default null, 4 | abbreviation varchar(50) default null, 5 | description varchar(50) default null 6 | ); 7 | 8 | create table reference_codes ( 9 | reference_type_id int(11), 10 | reference_code int(11) not null, 11 | code_label varchar(50) default null, 12 | abbreviation varchar(50) default null, 13 | description varchar(50) default null, 14 | primary key (reference_type_id, reference_code) 15 | ); 16 | 17 | create table products ( 18 | id int(11) not null primary key, 19 | name varchar(50) default null 20 | ); 21 | 22 | create table tariffs ( 23 | tariff_id int(11) not null, 24 | start_date date not null, 25 | amount integer(11) default null, 26 | primary key (tariff_id, start_date) 27 | ); 28 | 29 | create table product_tariffs ( 30 | product_id int(11) not null, 31 | tariff_id int(11) not null, 32 | tariff_start_date date not null, 33 | primary key (product_id, tariff_id, tariff_start_date) 34 | ); 35 | 36 | create table suburbs ( 37 | city_id int(11) not null, 38 | suburb_id int(11) not null, 39 | name varchar(50) not null, 40 | primary key (city_id, suburb_id) 41 | ); 42 | 43 | create table streets ( 44 | id integer not null primary key autoincrement, 45 | city_id int(11) not null, 46 | suburb_id int(11) not null, 47 | name varchar(50) not null 48 | ); 49 | 50 | create table users ( 51 | id integer not null primary key autoincrement, 52 | name varchar(50) not null 53 | ); 54 | 55 | create table articles ( 56 | id integer not null primary key autoincrement, 57 | name varchar(50) not null 58 | ); 59 | 60 | create table readings ( 61 | id integer not null primary key autoincrement, 62 | user_id int(11) not null, 63 | article_id int(11) not null, 64 | rating int(11) not null 65 | ); 66 | 67 | create table groups ( 68 | id integer not null primary key autoincrement, 69 | name varchar(50) not null 70 | ); 71 | 72 | create table memberships ( 73 | user_id int not null, 74 | group_id int not null, 75 | primary key (user_id, group_id) 76 | ); 77 | 78 | create table membership_statuses ( 79 | id integer not null primary key autoincrement, 80 | user_id int not null, 81 | group_id int not null, 82 | status varchar(50) not null 83 | ); 84 | 85 | create table departments ( 86 | department_id integer not null, 87 | location_id integer not null, 88 | primary key (department_id, location_id) 89 | ); 90 | 91 | create table employees ( 92 | id integer not null primary key autoincrement, 93 | department_id integer null, 94 | location_id integer null 95 | ); 96 | 97 | create table comments ( 98 | id integer not null primary key autoincrement, 99 | person_id varchar(100) null, 100 | person_type varchar(100) null, 101 | hack_id varchar(100) null 102 | ); 103 | 104 | create table hacks ( 105 | name varchar(50) not null primary key 106 | ); 107 | 108 | create table kitchen_sinks ( 109 | id_1 integer not null, 110 | id_2 integer not null, 111 | a_date date, 112 | a_string varchar(100), 113 | primary key (id_1, id_2) 114 | ); 115 | 116 | create table restaurants ( 117 | franchise_id integer not null, 118 | store_id integer not null, 119 | name varchar(100), 120 | primary key (franchise_id, store_id) 121 | ); 122 | 123 | create table restaurants_suburbs ( 124 | franchise_id integer not null, 125 | store_id integer not null, 126 | city_id integer not null, 127 | suburb_id integer not null 128 | ); 129 | 130 | create table dorms ( 131 | id integer not null primary key autoincrement 132 | ); 133 | 134 | create table rooms ( 135 | dorm_id integer not null, 136 | room_id integer not null, 137 | primary key (dorm_id, room_id) 138 | ); 139 | 140 | create table room_attributes ( 141 | id integer not null primary key autoincrement, 142 | name varchar(50) 143 | ); 144 | 145 | create table room_attribute_assignments ( 146 | dorm_id integer not null, 147 | room_id integer not null, 148 | room_attribute_id integer not null 149 | ); 150 | 151 | create table students ( 152 | id integer not null primary key autoincrement 153 | ); 154 | 155 | create table room_assignments ( 156 | student_id integer not null, 157 | dorm_id integer not null, 158 | room_id integer not null 159 | ); 160 | 161 | create table seats ( 162 | flight_number integer not_null, 163 | seat integer not_null, 164 | customer integer, 165 | primary key (flight_number, seat) 166 | ); 167 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/department.rb: -------------------------------------------------------------------------------- 1 | class Department < ActiveRecord::Base 2 | # set_primary_keys *keys - turns on composite key functionality 3 | set_primary_keys :department_id, :location_id 4 | has_many :employees, :foreign_key => [:department_id, :location_id] 5 | end 6 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/departments.yml: -------------------------------------------------------------------------------- 1 | department1-cpk: 2 | department_id: 1 3 | location_id: 1 4 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/dorm.rb: -------------------------------------------------------------------------------- 1 | class Dorm < ActiveRecord::Base 2 | has_many :rooms, :include => :room_attributes 3 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/dorms.yml: -------------------------------------------------------------------------------- 1 | jacksons_dorm: 2 | id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/employee.rb: -------------------------------------------------------------------------------- 1 | class Employee < ActiveRecord::Base 2 | belongs_to :department, :foreign_key => [:department_id, :location_id] 3 | has_many :comments, :as => :person 4 | end 5 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/employees.yml: -------------------------------------------------------------------------------- 1 | employee1: 2 | id: 1 3 | department_id: 1 4 | location_id: 1 5 | employee2: 6 | id: 2 7 | department_id: 1 8 | location_id: 1 9 | 10 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/group.rb: -------------------------------------------------------------------------------- 1 | class Group < ActiveRecord::Base 2 | has_many :memberships 3 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/groups.yml: -------------------------------------------------------------------------------- 1 | cpk: 2 | id: 1 3 | name: Composite Primary Keys -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/hack.rb: -------------------------------------------------------------------------------- 1 | class Hack < ActiveRecord::Base 2 | set_primary_keys :name 3 | has_many :comments, :as => :person 4 | 5 | has_one :first_comment, :as => :person, :class_name => "Comment" 6 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/hacks.yml: -------------------------------------------------------------------------------- 1 | andrew: 2 | name: andrew -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/kitchen_sink.rb: -------------------------------------------------------------------------------- 1 | class KitchenSink < ActiveRecord::Base 2 | set_primary_keys :id_1, :id_2 3 | end 4 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/kitchen_sinks.yml: -------------------------------------------------------------------------------- 1 | first: 2 | id_1: 1 3 | id_2: 2 4 | a_date: <%= Date.today.to_s(:db) %> 5 | a_string: 'string' 6 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/membership.rb: -------------------------------------------------------------------------------- 1 | class Membership < ActiveRecord::Base 2 | # set_primary_keys *keys - turns on composite key functionality 3 | set_primary_keys :user_id, :group_id 4 | belongs_to :user 5 | belongs_to :group 6 | has_many :statuses, :class_name => 'MembershipStatus', :foreign_key => [:user_id, :group_id] 7 | 8 | has_many :readings, :primary_key => :user_id, :foreign_key => :user_id 9 | has_one :reading, :primary_key => :user_id, :foreign_key => :user_id, :order => 'id DESC' 10 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/membership_status.rb: -------------------------------------------------------------------------------- 1 | class MembershipStatus < ActiveRecord::Base 2 | belongs_to :membership, :foreign_key => [:user_id, :group_id] 3 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/membership_statuses.yml: -------------------------------------------------------------------------------- 1 | santiago-cpk: 2 | id: 1 3 | user_id: 1 4 | group_id: 1 5 | status: Active 6 | drnic-cpk: 7 | id: 2 8 | user_id: 2 9 | group_id: 1 10 | status: Owner -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/memberships.yml: -------------------------------------------------------------------------------- 1 | santiago-cpk: 2 | user_id: 1 3 | group_id: 1 4 | drnic-cpk: 5 | user_id: 2 6 | group_id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ActiveRecord::Base 2 | set_primary_keys :id # redundant 3 | has_many :product_tariffs, :foreign_key => :product_id 4 | has_one :product_tariff, :foreign_key => :product_id 5 | 6 | has_many :tariffs, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date] 7 | end 8 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/product_tariff.rb: -------------------------------------------------------------------------------- 1 | class ProductTariff < ActiveRecord::Base 2 | set_primary_keys :product_id, :tariff_id, :tariff_start_date 3 | belongs_to :product, :foreign_key => :product_id 4 | belongs_to :tariff, :foreign_key => [:tariff_id, :tariff_start_date] 5 | end 6 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/product_tariffs.yml: -------------------------------------------------------------------------------- 1 | first_flat: 2 | product_id: 1 3 | tariff_id: 1 4 | tariff_start_date: <%= Date.today.to_s(:db) %> 5 | first_free: 6 | product_id: 1 7 | tariff_id: 2 8 | tariff_start_date: <%= Date.today.to_s(:db) %> 9 | second_free: 10 | product_id: 2 11 | tariff_id: 2 12 | tariff_start_date: <%= Date.today.to_s(:db) %> 13 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/products.yml: -------------------------------------------------------------------------------- 1 | first_product: 2 | id: 1 3 | name: Product One 4 | second_product: 5 | id: 2 6 | name: Product Two -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/reading.rb: -------------------------------------------------------------------------------- 1 | class Reading < ActiveRecord::Base 2 | belongs_to :article 3 | belongs_to :user 4 | end 5 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/readings.yml: -------------------------------------------------------------------------------- 1 | santiago_first: 2 | id: 1 3 | user_id: 1 4 | article_id: 1 5 | rating: 4 6 | santiago_second: 7 | id: 2 8 | user_id: 1 9 | article_id: 2 10 | rating: 5 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/reference_code.rb: -------------------------------------------------------------------------------- 1 | class ReferenceCode < ActiveRecord::Base 2 | set_primary_keys :reference_type_id, :reference_code 3 | 4 | belongs_to :reference_type, :foreign_key => "reference_type_id" 5 | 6 | validates_presence_of :reference_code, :code_label, :abbreviation 7 | end 8 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/reference_codes.yml: -------------------------------------------------------------------------------- 1 | name_prefix_mr: 2 | reference_type_id: 1 3 | reference_code: 1 4 | code_label: MR 5 | abbreviation: Mr 6 | name_prefix_mrs: 7 | reference_type_id: 1 8 | reference_code: 2 9 | code_label: MRS 10 | abbreviation: Mrs 11 | name_prefix_ms: 12 | reference_type_id: 1 13 | reference_code: 3 14 | code_label: MS 15 | abbreviation: Ms 16 | 17 | gender_male: 18 | reference_type_id: 2 19 | reference_code: 1 20 | code_label: MALE 21 | abbreviation: Male 22 | gender_female: 23 | reference_type_id: 2 24 | reference_code: 2 25 | code_label: FEMALE 26 | abbreviation: Female 27 | 28 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/reference_type.rb: -------------------------------------------------------------------------------- 1 | class ReferenceType < ActiveRecord::Base 2 | set_primary_key :reference_type_id 3 | has_many :reference_codes, :foreign_key => "reference_type_id" 4 | 5 | validates_presence_of :type_label, :abbreviation 6 | validates_uniqueness_of :type_label 7 | end 8 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/reference_types.yml: -------------------------------------------------------------------------------- 1 | name_prefix: 2 | reference_type_id: 1 3 | type_label: NAME_PREFIX 4 | abbreviation: Name Prefix 5 | 6 | gender: 7 | reference_type_id: 2 8 | type_label: GENDER 9 | abbreviation: Gender 10 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/restaurant.rb: -------------------------------------------------------------------------------- 1 | class Restaurant < ActiveRecord::Base 2 | set_primary_keys :franchise_id, :store_id 3 | has_and_belongs_to_many :suburbs, 4 | :foreign_key => [:franchise_id, :store_id], 5 | :association_foreign_key => [:city_id, :suburb_id] 6 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/restaurants.yml: -------------------------------------------------------------------------------- 1 | mcdonalds: 2 | franchise_id: 1 3 | store_id: 1 4 | name: McDonalds 5 | 6 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/restaurants_suburbs.yml: -------------------------------------------------------------------------------- 1 | a: 2 | franchise_id: 1 3 | store_id: 1 4 | city_id: 1 5 | suburb_id: 1 6 | 7 | b: 8 | franchise_id: 1 9 | store_id: 1 10 | city_id: 2 11 | suburb_id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room.rb: -------------------------------------------------------------------------------- 1 | class Room < ActiveRecord::Base 2 | set_primary_keys :dorm_id, :room_id 3 | belongs_to :dorm 4 | has_many :room_attribute_assignments, :foreign_key => [:dorm_id, :room_id] 5 | has_many :room_attributes, :through => :room_attribute_assignments 6 | 7 | def find_custom_room_attributes 8 | room_attributes.find(:all, :conditions => ["room_attributes.name != ?", "keg"]) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room_assignment.rb: -------------------------------------------------------------------------------- 1 | class RoomAssignment < ActiveRecord::Base 2 | belongs_to :student 3 | belongs_to :room, :foreign_key => [:dorm_id, :room_id] 4 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room_assignments.yml: -------------------------------------------------------------------------------- 1 | jacksons_room: 2 | student_id: 1 3 | dorm_id: 1 4 | room_id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room_attribute.rb: -------------------------------------------------------------------------------- 1 | class RoomAttribute < ActiveRecord::Base 2 | has_many :rooms, :through => :room_attribute_assignments, :foreign_key => [:dorm_id, :room_id] 3 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room_attribute_assignment.rb: -------------------------------------------------------------------------------- 1 | class RoomAttributeAssignment < ActiveRecord::Base 2 | set_primary_keys :dorm_id, :room_id, :room_attribute_id 3 | belongs_to :room, :foreign_key => [:dorm_id, :room_id] 4 | belongs_to :room_attribute 5 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room_attribute_assignments.yml: -------------------------------------------------------------------------------- 1 | assignment: 2 | dorm_id: 1 3 | room_id: 1 4 | room_attribute_id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/room_attributes.yml: -------------------------------------------------------------------------------- 1 | attribute_1: 2 | id: 1 3 | name: 'keg' -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/rooms.yml: -------------------------------------------------------------------------------- 1 | jacksons_room: 2 | dorm_id: 1 3 | room_id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/seat.rb: -------------------------------------------------------------------------------- 1 | class Seat < ActiveRecord::Base 2 | set_primary_keys [:flight_number, :seat] 3 | 4 | validates_uniqueness_of :customer 5 | end 6 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/seats.yml: -------------------------------------------------------------------------------- 1 | seat1: 2 | flight_number: 1 3 | seat: 1 4 | customer: 1 5 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/street.rb: -------------------------------------------------------------------------------- 1 | class Street < ActiveRecord::Base 2 | belongs_to :suburb, :foreign_key => [:city_id, :suburb_id] 3 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/streets.yml: -------------------------------------------------------------------------------- 1 | first: 2 | id: 1 3 | city_id: 1 4 | suburb_id: 1 5 | name: First Street 6 | second1: 7 | id: 2 8 | city_id: 2 9 | suburb_id: 1 10 | name: First Street 11 | second2: 12 | id: 3 13 | city_id: 2 14 | suburb_id: 1 15 | name: Second Street -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/student.rb: -------------------------------------------------------------------------------- 1 | class Student < ActiveRecord::Base 2 | has_many :room_assignments 3 | has_many :rooms, :through => :room_assignments, :foreign_key => [:building_code, :room_number] 4 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/students.yml: -------------------------------------------------------------------------------- 1 | jackson: 2 | id: 1 -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/suburb.rb: -------------------------------------------------------------------------------- 1 | class Suburb < ActiveRecord::Base 2 | set_primary_keys :city_id, :suburb_id 3 | has_many :streets, :foreign_key => [:city_id, :suburb_id] 4 | has_many :first_streets, :foreign_key => [:city_id, :suburb_id], 5 | :class_name => 'Street', :conditions => "streets.name = 'First Street'" 6 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/suburbs.yml: -------------------------------------------------------------------------------- 1 | first: 2 | city_id: 1 3 | suburb_id: 1 4 | name: First Suburb 5 | second: 6 | city_id: 2 7 | suburb_id: 1 8 | name: Second Suburb 9 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/tariff.rb: -------------------------------------------------------------------------------- 1 | class Tariff < ActiveRecord::Base 2 | set_primary_keys [:tariff_id, :start_date] 3 | has_many :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date] 4 | has_one :product_tariff, :foreign_key => [:tariff_id, :tariff_start_date] 5 | has_many :products, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date] 6 | end 7 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/tariffs.yml: -------------------------------------------------------------------------------- 1 | flat: 2 | tariff_id: 1 3 | start_date: <%= Date.today.to_s(:db) %> 4 | amount: 50 5 | free: 6 | tariff_id: 2 7 | start_date: <%= Date.today.to_s(:db) %> 8 | amount: 0 9 | flat_future: 10 | tariff_id: 1 11 | start_date: <%= Date.today.next.to_s(:db) %> 12 | amount: 100 13 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :readings 3 | has_many :articles, :through => :readings 4 | has_many :comments, :as => :person 5 | has_many :hacks, :through => :comments, :source => :hack 6 | 7 | def find_custom_articles 8 | articles.find(:all, :conditions => ["name = ?", "Article One"]) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /composite_primary_keys/test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | santiago: 2 | id: 1 3 | name: Santiago 4 | drnic: 5 | id: 2 6 | name: Dr Nic -------------------------------------------------------------------------------- /composite_primary_keys/test/hash_tricks.rb: -------------------------------------------------------------------------------- 1 | # From: 2 | # http://www.bigbold.com/snippets/posts/show/2178 3 | # http://blog.caboo.se/articles/2006/06/11/stupid-hash-tricks 4 | # 5 | # An example utilisation of these methods in a controller is: 6 | # def some_action 7 | # # some script kiddie also passed in :bee, which we don't want tampered with _here_. 8 | # @model = Model.create(params.pass(:foo, :bar)) 9 | # end 10 | class Hash 11 | 12 | # lets through the keys in the argument 13 | # >> {:one => 1, :two => 2, :three => 3}.pass(:one) 14 | # => {:one=>1} 15 | def pass(*keys) 16 | keys = keys.first if keys.first.is_a?(Array) 17 | tmp = self.clone 18 | tmp.delete_if {|k,v| ! keys.include?(k.to_sym) } 19 | tmp.delete_if {|k,v| ! keys.include?(k.to_s) } 20 | tmp 21 | end 22 | 23 | # blocks the keys in the arguments 24 | # >> {:one => 1, :two => 2, :three => 3}.block(:one) 25 | # => {:two=>2, :three=>3} 26 | def block(*keys) 27 | keys = keys.first if keys.first.is_a?(Array) 28 | tmp = self.clone 29 | tmp.delete_if {|k,v| keys.include?(k.to_sym) } 30 | tmp.delete_if {|k,v| keys.include?(k.to_s) } 31 | tmp 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /composite_primary_keys/test/plugins/pagination.rb: -------------------------------------------------------------------------------- 1 | module ActionController 2 | # === Action Pack pagination for Active Record collections 3 | # 4 | # The Pagination module aids in the process of paging large collections of 5 | # Active Record objects. It offers macro-style automatic fetching of your 6 | # model for multiple views, or explicit fetching for single actions. And if 7 | # the magic isn't flexible enough for your needs, you can create your own 8 | # paginators with a minimal amount of code. 9 | # 10 | # The Pagination module can handle as much or as little as you wish. In the 11 | # controller, have it automatically query your model for pagination; or, 12 | # if you prefer, create Paginator objects yourself. 13 | # 14 | # Pagination is included automatically for all controllers. 15 | # 16 | # For help rendering pagination links, see 17 | # ActionView::Helpers::PaginationHelper. 18 | # 19 | # ==== Automatic pagination for every action in a controller 20 | # 21 | # class PersonController < ApplicationController 22 | # model :person 23 | # 24 | # paginate :people, :order => 'last_name, first_name', 25 | # :per_page => 20 26 | # 27 | # # ... 28 | # end 29 | # 30 | # Each action in this controller now has access to a @people 31 | # instance variable, which is an ordered collection of model objects for the 32 | # current page (at most 20, sorted by last name and first name), and a 33 | # @person_pages Paginator instance. The current page is determined 34 | # by the params[:page] variable. 35 | # 36 | # ==== Pagination for a single action 37 | # 38 | # def list 39 | # @person_pages, @people = 40 | # paginate :people, :order => 'last_name, first_name' 41 | # end 42 | # 43 | # Like the previous example, but explicitly creates @person_pages 44 | # and @people for a single action, and uses the default of 10 items 45 | # per page. 46 | # 47 | # ==== Custom/"classic" pagination 48 | # 49 | # def list 50 | # @person_pages = Paginator.new self, Person.count, 10, params[:page] 51 | # @people = Person.find :all, :order => 'last_name, first_name', 52 | # :limit => @person_pages.items_per_page, 53 | # :offset => @person_pages.current.offset 54 | # end 55 | # 56 | # Explicitly creates the paginator from the previous example and uses 57 | # Paginator#to_sql to retrieve @people from the model. 58 | # 59 | module Pagination 60 | unless const_defined?(:OPTIONS) 61 | # A hash holding options for controllers using macro-style pagination 62 | OPTIONS = Hash.new 63 | 64 | # The default options for pagination 65 | DEFAULT_OPTIONS = { 66 | :class_name => nil, 67 | :singular_name => nil, 68 | :per_page => 10, 69 | :conditions => nil, 70 | :order_by => nil, 71 | :order => nil, 72 | :join => nil, 73 | :joins => nil, 74 | :count => nil, 75 | :include => nil, 76 | :select => nil, 77 | :group => nil, 78 | :parameter => 'page' 79 | } 80 | else 81 | DEFAULT_OPTIONS[:group] = nil 82 | end 83 | 84 | def self.included(base) #:nodoc: 85 | super 86 | base.extend(ClassMethods) 87 | end 88 | 89 | def self.validate_options!(collection_id, options, in_action) #:nodoc: 90 | options.merge!(DEFAULT_OPTIONS) {|key, old, new| old} 91 | 92 | valid_options = DEFAULT_OPTIONS.keys 93 | valid_options << :actions unless in_action 94 | 95 | unknown_option_keys = options.keys - valid_options 96 | raise ActionController::ActionControllerError, 97 | "Unknown options: #{unknown_option_keys.join(', ')}" unless 98 | unknown_option_keys.empty? 99 | 100 | options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s) 101 | options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name]) 102 | end 103 | 104 | # Returns a paginator and a collection of Active Record model instances 105 | # for the paginator's current page. This is designed to be used in a 106 | # single action; to automatically paginate multiple actions, consider 107 | # ClassMethods#paginate. 108 | # 109 | # +options+ are: 110 | # :singular_name:: the singular name to use, if it can't be inferred by singularizing the collection name 111 | # :class_name:: the class name to use, if it can't be inferred by 112 | # camelizing the singular name 113 | # :per_page:: the maximum number of items to include in a 114 | # single page. Defaults to 10 115 | # :conditions:: optional conditions passed to Model.find(:all, *params) and 116 | # Model.count 117 | # :order:: optional order parameter passed to Model.find(:all, *params) 118 | # :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) 119 | # :joins:: optional joins parameter passed to Model.find(:all, *params) 120 | # and Model.count 121 | # :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) 122 | # and Model.count 123 | # :include:: optional eager loading parameter passed to Model.find(:all, *params) 124 | # and Model.count 125 | # :select:: :select parameter passed to Model.find(:all, *params) 126 | # 127 | # :count:: parameter passed as :select option to Model.count(*params) 128 | # 129 | # :group:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records 130 | # 131 | def paginate(collection_id, options={}) 132 | Pagination.validate_options!(collection_id, options, true) 133 | paginator_and_collection_for(collection_id, options) 134 | end 135 | 136 | # These methods become class methods on any controller 137 | module ClassMethods 138 | # Creates a +before_filter+ which automatically paginates an Active 139 | # Record model for all actions in a controller (or certain actions if 140 | # specified with the :actions option). 141 | # 142 | # +options+ are the same as PaginationHelper#paginate, with the addition 143 | # of: 144 | # :actions:: an array of actions for which the pagination is 145 | # active. Defaults to +nil+ (i.e., every action) 146 | def paginate(collection_id, options={}) 147 | Pagination.validate_options!(collection_id, options, false) 148 | module_eval do 149 | before_filter :create_paginators_and_retrieve_collections 150 | OPTIONS[self] ||= Hash.new 151 | OPTIONS[self][collection_id] = options 152 | end 153 | end 154 | end 155 | 156 | def create_paginators_and_retrieve_collections #:nodoc: 157 | Pagination::OPTIONS[self.class].each do |collection_id, options| 158 | next unless options[:actions].include? action_name if 159 | options[:actions] 160 | 161 | paginator, collection = 162 | paginator_and_collection_for(collection_id, options) 163 | 164 | paginator_name = "@#{options[:singular_name]}_pages" 165 | self.instance_variable_set(paginator_name, paginator) 166 | 167 | collection_name = "@#{collection_id.to_s}" 168 | self.instance_variable_set(collection_name, collection) 169 | end 170 | end 171 | 172 | # Returns the total number of items in the collection to be paginated for 173 | # the +model+ and given +conditions+. Override this method to implement a 174 | # custom counter. 175 | def count_collection_for_pagination(model, options) 176 | model.count(:conditions => options[:conditions], 177 | :joins => options[:join] || options[:joins], 178 | :include => options[:include], 179 | :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count])) 180 | end 181 | 182 | # Returns a collection of items for the given +model+ and +options[conditions]+, 183 | # ordered by +options[order]+, for the current page in the given +paginator+. 184 | # Override this method to implement a custom finder. 185 | def find_collection_for_pagination(model, options, paginator) 186 | model.find(:all, :conditions => options[:conditions], 187 | :order => options[:order_by] || options[:order], 188 | :joins => options[:join] || options[:joins], :include => options[:include], 189 | :select => options[:select], :limit => options[:per_page], 190 | :group => options[:group], :offset => paginator.current.offset) 191 | end 192 | 193 | protected :create_paginators_and_retrieve_collections, 194 | :count_collection_for_pagination, 195 | :find_collection_for_pagination 196 | 197 | def paginator_and_collection_for(collection_id, options) #:nodoc: 198 | klass = options[:class_name].constantize 199 | page = params[options[:parameter]] 200 | count = count_collection_for_pagination(klass, options) 201 | paginator = Paginator.new(self, count, options[:per_page], page) 202 | collection = find_collection_for_pagination(klass, options, paginator) 203 | 204 | return paginator, collection 205 | end 206 | 207 | private :paginator_and_collection_for 208 | 209 | # A class representing a paginator for an Active Record collection. 210 | class Paginator 211 | include Enumerable 212 | 213 | # Creates a new Paginator on the given +controller+ for a set of items 214 | # of size +item_count+ and having +items_per_page+ items per page. 215 | # Raises ArgumentError if items_per_page is out of bounds (i.e., less 216 | # than or equal to zero). The page CGI parameter for links defaults to 217 | # "page" and can be overridden with +page_parameter+. 218 | def initialize(controller, item_count, items_per_page, current_page=1) 219 | raise ArgumentError, 'must have at least one item per page' if 220 | items_per_page <= 0 221 | 222 | @controller = controller 223 | @item_count = item_count || 0 224 | @items_per_page = items_per_page 225 | @pages = {} 226 | 227 | self.current_page = current_page 228 | end 229 | attr_reader :controller, :item_count, :items_per_page 230 | 231 | # Sets the current page number of this paginator. If +page+ is a Page 232 | # object, its +number+ attribute is used as the value; if the page does 233 | # not belong to this Paginator, an ArgumentError is raised. 234 | def current_page=(page) 235 | if page.is_a? Page 236 | raise ArgumentError, 'Page/Paginator mismatch' unless 237 | page.paginator == self 238 | end 239 | page = page.to_i 240 | @current_page_number = has_page_number?(page) ? page : 1 241 | end 242 | 243 | # Returns a Page object representing this paginator's current page. 244 | def current_page 245 | @current_page ||= self[@current_page_number] 246 | end 247 | alias current :current_page 248 | 249 | # Returns a new Page representing the first page in this paginator. 250 | def first_page 251 | @first_page ||= self[1] 252 | end 253 | alias first :first_page 254 | 255 | # Returns a new Page representing the last page in this paginator. 256 | def last_page 257 | @last_page ||= self[page_count] 258 | end 259 | alias last :last_page 260 | 261 | # Returns the number of pages in this paginator. 262 | def page_count 263 | @page_count ||= @item_count.zero? ? 1 : 264 | (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1) 265 | end 266 | 267 | alias length :page_count 268 | 269 | # Returns true if this paginator contains the page of index +number+. 270 | def has_page_number?(number) 271 | number >= 1 and number <= page_count 272 | end 273 | 274 | # Returns a new Page representing the page with the given index 275 | # +number+. 276 | def [](number) 277 | @pages[number] ||= Page.new(self, number) 278 | end 279 | 280 | # Successively yields all the paginator's pages to the given block. 281 | def each(&block) 282 | page_count.times do |n| 283 | yield self[n+1] 284 | end 285 | end 286 | 287 | # A class representing a single page in a paginator. 288 | class Page 289 | include Comparable 290 | 291 | # Creates a new Page for the given +paginator+ with the index 292 | # +number+. If +number+ is not in the range of valid page numbers or 293 | # is not a number at all, it defaults to 1. 294 | def initialize(paginator, number) 295 | @paginator = paginator 296 | @number = number.to_i 297 | @number = 1 unless @paginator.has_page_number? @number 298 | end 299 | attr_reader :paginator, :number 300 | alias to_i :number 301 | 302 | # Compares two Page objects and returns true when they represent the 303 | # same page (i.e., their paginators are the same and they have the 304 | # same page number). 305 | def ==(page) 306 | return false if page.nil? 307 | @paginator == page.paginator and 308 | @number == page.number 309 | end 310 | 311 | # Compares two Page objects and returns -1 if the left-hand page comes 312 | # before the right-hand page, 0 if the pages are equal, and 1 if the 313 | # left-hand page comes after the right-hand page. Raises ArgumentError 314 | # if the pages do not belong to the same Paginator object. 315 | def <=>(page) 316 | raise ArgumentError unless @paginator == page.paginator 317 | @number <=> page.number 318 | end 319 | 320 | # Returns the item offset for the first item in this page. 321 | def offset 322 | @paginator.items_per_page * (@number - 1) 323 | end 324 | 325 | # Returns the number of the first item displayed. 326 | def first_item 327 | offset + 1 328 | end 329 | 330 | # Returns the number of the last item displayed. 331 | def last_item 332 | [@paginator.items_per_page * @number, @paginator.item_count].min 333 | end 334 | 335 | # Returns true if this page is the first page in the paginator. 336 | def first? 337 | self == @paginator.first 338 | end 339 | 340 | # Returns true if this page is the last page in the paginator. 341 | def last? 342 | self == @paginator.last 343 | end 344 | 345 | # Returns a new Page object representing the page just before this 346 | # page, or nil if this is the first page. 347 | def previous 348 | if first? then nil else @paginator[@number - 1] end 349 | end 350 | 351 | # Returns a new Page object representing the page just after this 352 | # page, or nil if this is the last page. 353 | def next 354 | if last? then nil else @paginator[@number + 1] end 355 | end 356 | 357 | # Returns a new Window object for this page with the specified 358 | # +padding+. 359 | def window(padding=2) 360 | Window.new(self, padding) 361 | end 362 | 363 | # Returns the limit/offset array for this page. 364 | def to_sql 365 | [@paginator.items_per_page, offset] 366 | end 367 | 368 | def to_param #:nodoc: 369 | @number.to_s 370 | end 371 | end 372 | 373 | # A class for representing ranges around a given page. 374 | class Window 375 | # Creates a new Window object for the given +page+ with the specified 376 | # +padding+. 377 | def initialize(page, padding=2) 378 | @paginator = page.paginator 379 | @page = page 380 | self.padding = padding 381 | end 382 | attr_reader :paginator, :page 383 | 384 | # Sets the window's padding (the number of pages on either side of the 385 | # window page). 386 | def padding=(padding) 387 | @padding = padding < 0 ? 0 : padding 388 | # Find the beginning and end pages of the window 389 | @first = @paginator.has_page_number?(@page.number - @padding) ? 390 | @paginator[@page.number - @padding] : @paginator.first 391 | @last = @paginator.has_page_number?(@page.number + @padding) ? 392 | @paginator[@page.number + @padding] : @paginator.last 393 | end 394 | attr_reader :padding, :first, :last 395 | 396 | # Returns an array of Page objects in the current window. 397 | def pages 398 | (@first.number..@last.number).to_a.collect! {|n| @paginator[n]} 399 | end 400 | alias to_a :pages 401 | end 402 | end 403 | 404 | end 405 | end 406 | -------------------------------------------------------------------------------- /composite_primary_keys/test/plugins/pagination_helper.rb: -------------------------------------------------------------------------------- 1 | module ActionView 2 | module Helpers 3 | # Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally 4 | # also build your links manually using ActionView::Helpers::AssetHelper#link_to like so: 5 | # 6 | # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %> 7 | # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %> 8 | module PaginationHelper 9 | unless const_defined?(:DEFAULT_OPTIONS) 10 | DEFAULT_OPTIONS = { 11 | :name => :page, 12 | :window_size => 2, 13 | :always_show_anchors => true, 14 | :link_to_current_page => false, 15 | :params => {} 16 | } 17 | end 18 | 19 | # Creates a basic HTML link bar for the given +paginator+. Links will be created 20 | # for the next and/or previous page and for a number of other pages around the current 21 | # pages position. The +html_options+ hash is passed to +link_to+ when the links are created. 22 | # 23 | # ==== Options 24 | # :name:: the routing name for this paginator 25 | # (defaults to +page+) 26 | # :prefix:: prefix for pagination links 27 | # (i.e. Older Pages: 1 2 3 4) 28 | # :suffix:: suffix for pagination links 29 | # (i.e. 1 2 3 4 <- Older Pages) 30 | # :window_size:: the number of pages to show around 31 | # the current page (defaults to 2) 32 | # :always_show_anchors:: whether or not the first and last 33 | # pages should always be shown 34 | # (defaults to +true+) 35 | # :link_to_current_page:: whether or not the current page 36 | # should be linked to (defaults to 37 | # +false+) 38 | # :params:: any additional routing parameters 39 | # for page URLs 40 | # 41 | # ==== Examples 42 | # # We'll assume we have a paginator setup in @person_pages... 43 | # 44 | # pagination_links(@person_pages) 45 | # # => 1 2 3 ... 10 46 | # 47 | # pagination_links(@person_pages, :link_to_current_page => true) 48 | # # => 1 2 3 ... 10 49 | # 50 | # pagination_links(@person_pages, :always_show_anchors => false) 51 | # # => 1 2 3 52 | # 53 | # pagination_links(@person_pages, :window_size => 1) 54 | # # => 1 2 ... 10 55 | # 56 | # pagination_links(@person_pages, :params => { :viewer => "flash" }) 57 | # # => 1 2 3 ... 58 | # # 10 59 | def pagination_links(paginator, options={}, html_options={}) 60 | name = options[:name] || DEFAULT_OPTIONS[:name] 61 | params = (options[:params] || DEFAULT_OPTIONS[:params]).clone 62 | 63 | prefix = options[:prefix] || '' 64 | suffix = options[:suffix] || '' 65 | 66 | pagination_links_each(paginator, options, prefix, suffix) do |n| 67 | params[name] = n 68 | link_to(n.to_s, params, html_options) 69 | end 70 | end 71 | 72 | # Iterate through the pages of a given +paginator+, invoking a 73 | # block for each page number that needs to be rendered as a link. 74 | # 75 | # ==== Options 76 | # :window_size:: the number of pages to show around 77 | # the current page (defaults to +2+) 78 | # :always_show_anchors:: whether or not the first and last 79 | # pages should always be shown 80 | # (defaults to +true+) 81 | # :link_to_current_page:: whether or not the current page 82 | # should be linked to (defaults to 83 | # +false+) 84 | # 85 | # ==== Example 86 | # # Turn paginated links into an Ajax call 87 | # pagination_links_each(paginator, page_options) do |link| 88 | # options = { :url => {:action => 'list'}, :update => 'results' } 89 | # html_options = { :href => url_for(:action => 'list') } 90 | # 91 | # link_to_remote(link.to_s, options, html_options) 92 | # end 93 | def pagination_links_each(paginator, options, prefix = nil, suffix = nil) 94 | options = DEFAULT_OPTIONS.merge(options) 95 | link_to_current_page = options[:link_to_current_page] 96 | always_show_anchors = options[:always_show_anchors] 97 | 98 | current_page = paginator.current_page 99 | window_pages = current_page.window(options[:window_size]).pages 100 | return if window_pages.length <= 1 unless link_to_current_page 101 | 102 | first, last = paginator.first, paginator.last 103 | 104 | html = '' 105 | 106 | html << prefix if prefix 107 | 108 | if always_show_anchors and not (wp_first = window_pages[0]).first? 109 | html << yield(first.number) 110 | html << ' ... ' if wp_first.number - first.number > 1 111 | html << ' ' 112 | end 113 | 114 | window_pages.each do |page| 115 | if current_page == page && !link_to_current_page 116 | html << page.number.to_s 117 | else 118 | html << yield(page.number) 119 | end 120 | html << ' ' 121 | end 122 | 123 | if always_show_anchors and not (wp_last = window_pages[-1]).last? 124 | html << ' ... ' if last.number - wp_last.number > 1 125 | html << yield(last.number) 126 | end 127 | 128 | html << suffix if suffix 129 | 130 | html 131 | end 132 | 133 | end # PaginationHelper 134 | end # Helpers 135 | end # ActionView 136 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_associations.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/article' 3 | require 'fixtures/product' 4 | require 'fixtures/tariff' 5 | require 'fixtures/product_tariff' 6 | require 'fixtures/suburb' 7 | require 'fixtures/street' 8 | require 'fixtures/restaurant' 9 | require 'fixtures/dorm' 10 | require 'fixtures/room' 11 | require 'fixtures/room_attribute' 12 | require 'fixtures/room_attribute_assignment' 13 | require 'fixtures/student' 14 | require 'fixtures/room_assignment' 15 | require 'fixtures/user' 16 | require 'fixtures/reading' 17 | 18 | class TestAssociations < ActiveSupport::TestCase 19 | fixtures :articles, :products, :tariffs, :product_tariffs, :suburbs, :streets, :restaurants, :restaurants_suburbs, 20 | :dorms, :rooms, :room_attributes, :room_attribute_assignments, :students, :room_assignments, :users, :readings, 21 | :memberships 22 | 23 | def test_has_many_through_with_conditions_when_through_association_is_not_composite 24 | user = User.find(:first) 25 | assert_equal 1, user.articles.find(:all, :conditions => ["articles.name = ?", "Article One"]).size 26 | end 27 | 28 | def test_has_many_through_with_conditions_when_through_association_is_composite 29 | room = Room.find(:first) 30 | assert_equal 0, room.room_attributes.find(:all, :conditions => ["room_attributes.name != ?", "keg"]).size 31 | end 32 | 33 | def test_has_many_through_on_custom_finder_when_through_association_is_composite_finder_when_through_association_is_not_composite 34 | user = User.find(:first) 35 | assert_equal 1, user.find_custom_articles.size 36 | end 37 | 38 | def test_has_many_through_on_custom_finder_when_through_association_is_composite 39 | room = Room.find(:first) 40 | assert_equal 0, room.find_custom_room_attributes.size 41 | end 42 | 43 | def test_count 44 | assert_equal 2, Product.count(:include => :product_tariffs) 45 | assert_equal 3, Tariff.count(:include => :product_tariffs) 46 | assert_equal 2, Tariff.count(:group => :start_date).size 47 | end 48 | 49 | def test_products 50 | assert_not_nil products(:first_product).product_tariffs 51 | assert_equal 2, products(:first_product).product_tariffs.length 52 | assert_not_nil products(:first_product).tariffs 53 | assert_equal 2, products(:first_product).tariffs.length 54 | assert_not_nil products(:first_product).product_tariff 55 | end 56 | 57 | def test_product_tariffs 58 | assert_not_nil product_tariffs(:first_flat).product 59 | assert_not_nil product_tariffs(:first_flat).tariff 60 | assert_equal Product, product_tariffs(:first_flat).product.class 61 | assert_equal Tariff, product_tariffs(:first_flat).tariff.class 62 | end 63 | 64 | def test_tariffs 65 | assert_not_nil tariffs(:flat).product_tariffs 66 | assert_equal 1, tariffs(:flat).product_tariffs.length 67 | assert_not_nil tariffs(:flat).products 68 | assert_equal 1, tariffs(:flat).products.length 69 | assert_not_nil tariffs(:flat).product_tariff 70 | end 71 | 72 | # Its not generating the instances of associated classes from the rows 73 | def test_find_includes_products 74 | assert @products = Product.find(:all, :include => :product_tariffs) 75 | assert_equal 2, @products.length 76 | assert_not_nil @products.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array' 77 | assert_equal 3, @products.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, 78 | "Incorrect number of product_tariffs returned" 79 | end 80 | 81 | def test_find_includes_tariffs 82 | assert @tariffs = Tariff.find(:all, :include => :product_tariffs) 83 | assert_equal 3, @tariffs.length 84 | assert_not_nil @tariffs.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array' 85 | assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, 86 | "Incorrect number of product_tariffs returnedturned" 87 | end 88 | 89 | def test_find_includes_product 90 | assert @product_tariffs = ProductTariff.find(:all, :include => :product) 91 | assert_equal 3, @product_tariffs.length 92 | assert_not_nil @product_tariffs.first.instance_variable_get('@product'), '@product not set' 93 | end 94 | 95 | def test_find_includes_comp_belongs_to_tariff 96 | assert @product_tariffs = ProductTariff.find(:all, :include => :tariff) 97 | assert_equal 3, @product_tariffs.length 98 | assert_not_nil @product_tariffs.first.instance_variable_get('@tariff'), '@tariff not set' 99 | end 100 | 101 | def test_find_includes_extended 102 | assert @products = Product.find(:all, :include => {:product_tariffs => :tariff}) 103 | assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@product_tariffs').length}, 104 | "Incorrect number of product_tariffs returned" 105 | 106 | assert @tariffs = Tariff.find(:all, :include => {:product_tariffs => :product}) 107 | assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, 108 | "Incorrect number of product_tariffs returned" 109 | end 110 | 111 | def test_join_where_clause 112 | @product = Product.find(:first, :include => :product_tariffs) 113 | where_clause = @product.product_tariffs.composite_where_clause( 114 | ['foo','bar'], [1,2] 115 | ) 116 | assert_equal('(foo=1 AND bar=2)', where_clause) 117 | end 118 | 119 | def test_has_many_through 120 | @products = Product.find(:all, :include => :tariffs) 121 | assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@tariffs').length}, 122 | "Incorrect number of tariffs returned" 123 | end 124 | 125 | def test_has_many_through_when_not_pre_loaded 126 | student = Student.find(:first) 127 | rooms = student.rooms 128 | assert_equal 1, rooms.size 129 | assert_equal 1, rooms.first.dorm_id 130 | assert_equal 1, rooms.first.room_id 131 | end 132 | 133 | def test_has_many_through_when_through_association_is_composite 134 | dorm = Dorm.find(:first) 135 | assert_equal 1, dorm.rooms.length 136 | assert_equal 1, dorm.rooms.first.room_attributes.length 137 | assert_equal 'keg', dorm.rooms.first.room_attributes.first.name 138 | end 139 | 140 | def test_associations_with_conditions 141 | @suburb = Suburb.find([2, 1]) 142 | assert_equal 2, @suburb.streets.size 143 | 144 | @suburb = Suburb.find([2, 1]) 145 | assert_equal 1, @suburb.first_streets.size 146 | 147 | @suburb = Suburb.find([2, 1], :include => :streets) 148 | assert_equal 2, @suburb.streets.size 149 | 150 | @suburb = Suburb.find([2, 1], :include => :first_streets) 151 | assert_equal 1, @suburb.first_streets.size 152 | end 153 | 154 | def test_has_and_belongs_to_many 155 | @restaurant = Restaurant.find([1,1]) 156 | assert_equal 2, @restaurant.suburbs.size 157 | 158 | @restaurant = Restaurant.find([1,1], :include => :suburbs) 159 | assert_equal 2, @restaurant.suburbs.size 160 | end 161 | 162 | def test_has_many_with_primary_key 163 | @membership = Membership.find([1, 1]) 164 | 165 | assert_equal 2, @membership.readings.size 166 | end 167 | 168 | def test_has_one_with_primary_key 169 | @membership = Membership.find([1, 1]) 170 | 171 | assert_equal 2, @membership.reading.id 172 | end 173 | 174 | def test_joins_has_many_with_primary_key 175 | @membership = Membership.find(:first, :joins => :readings, :conditions => { :readings => { :id => 1 } }) 176 | 177 | assert_equal [1, 1], @membership.id 178 | end 179 | 180 | def test_joins_has_one_with_primary_key 181 | @membership = Membership.find(:first, :joins => :reading, :conditions => { :readings => { :id => 2 } }) 182 | 183 | assert_equal [1, 1], @membership.id 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_attribute_methods.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/kitchen_sink' 3 | require 'fixtures/reference_type' 4 | 5 | class TestAttributeMethods < ActiveSupport::TestCase 6 | fixtures :kitchen_sinks, :reference_types 7 | 8 | def test_read_attribute_with_single_key 9 | rt = ReferenceType.find(1) 10 | assert_equal(1, rt.reference_type_id) 11 | assert_equal('NAME_PREFIX', rt.type_label) 12 | assert_equal('Name Prefix', rt.abbreviation) 13 | end 14 | 15 | def test_read_attribute_with_composite_keys 16 | sink = KitchenSink.find(1,2) 17 | assert_equal(1, sink.id_1) 18 | assert_equal(2, sink.id_2) 19 | assert_equal(Date.today, sink.a_date.to_date) 20 | assert_equal('string', sink.a_string) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_attributes.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | require 'fixtures/product' 5 | require 'fixtures/tariff' 6 | require 'fixtures/product_tariff' 7 | 8 | class TestAttributes < ActiveSupport::TestCase 9 | fixtures :reference_types, :reference_codes, :products, :tariffs, :product_tariffs 10 | 11 | CLASSES = { 12 | :single => { 13 | :class => ReferenceType, 14 | :primary_keys => :reference_type_id, 15 | }, 16 | :dual => { 17 | :class => ReferenceCode, 18 | :primary_keys => [:reference_type_id, :reference_code], 19 | }, 20 | } 21 | 22 | def setup 23 | self.class.classes = CLASSES 24 | end 25 | 26 | def test_brackets 27 | testing_with do 28 | @first.attributes.each_pair do |attr_name, value| 29 | assert_equal value, @first[attr_name] 30 | end 31 | end 32 | end 33 | 34 | def test_brackets_primary_key 35 | testing_with do 36 | assert_equal @first.id, @first[@primary_keys], "[] failing for #{@klass}" 37 | assert_equal @first.id, @first[@first.class.primary_key] 38 | end 39 | end 40 | 41 | def test_brackets_assignment 42 | testing_with do 43 | @first.attributes.each_pair do |attr_name, value| 44 | @first[attr_name]= !value.nil? ? value * 2 : '1' 45 | assert_equal !value.nil? ? value * 2 : '1', @first[attr_name] 46 | end 47 | end 48 | end 49 | 50 | def test_brackets_foreign_key_assignment 51 | @flat = Tariff.find(1, Date.today.to_s(:db)) 52 | @second_free = ProductTariff.find(2,2,Date.today.to_s(:db)) 53 | @second_free_fk = [:tariff_id, :tariff_start_date] 54 | @second_free[key = @second_free_fk] = @flat.id 55 | compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk) 56 | assert_equal @flat.id, @second_free[key] 57 | @second_free[key = @second_free_fk.to_composite_ids] = @flat.id 58 | assert_equal @flat.id, @second_free[key] 59 | compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk) 60 | @second_free[key = @second_free_fk.to_composite_ids] = @flat.id.to_s 61 | assert_equal @flat.id, @second_free[key] 62 | compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk) 63 | @second_free[key = @second_free_fk.to_composite_ids] = @flat.id.to_s 64 | assert_equal @flat.id, @second_free[key] 65 | compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk) 66 | @second_free[key = @second_free_fk.to_composite_ids.to_s] = @flat.id 67 | assert_equal @flat.id, @second_free[key] 68 | compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk) 69 | @second_free[key = @second_free_fk.to_composite_ids.to_s] = @flat.id.to_s 70 | assert_equal @flat.id, @second_free[key] 71 | compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk) 72 | end 73 | private 74 | def compare_indexes(obj_name1, indexes1, obj_name2, indexes2) 75 | obj1, obj2 = eval "[#{obj_name1}, #{obj_name2}]" 76 | indexes1.length.times do |key_index| 77 | assert_equal obj1[indexes1[key_index].to_s], 78 | obj2[indexes2[key_index].to_s], 79 | "#{obj_name1}[#{indexes1[key_index]}]=#{obj1[indexes1[key_index].to_s].inspect} != " + 80 | "#{obj_name2}[#{indexes2[key_index]}]=#{obj2[indexes2[key_index].to_s].inspect}; " + 81 | "#{obj_name2} = #{obj2.inspect}" 82 | end 83 | end 84 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_clone.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | class TestClone < ActiveSupport::TestCase 6 | fixtures :reference_types, :reference_codes 7 | 8 | CLASSES = { 9 | :single => { 10 | :class => ReferenceType, 11 | :primary_keys => :reference_type_id, 12 | }, 13 | :dual => { 14 | :class => ReferenceCode, 15 | :primary_keys => [:reference_type_id, :reference_code], 16 | }, 17 | } 18 | 19 | def setup 20 | self.class.classes = CLASSES 21 | end 22 | 23 | def test_truth 24 | testing_with do 25 | clone = @first.clone 26 | assert_equal @first.attributes.block(@klass.primary_key), clone.attributes 27 | if composite? 28 | @klass.primary_key.each {|key| assert_nil clone[key], "Primary key '#{key}' should be nil"} 29 | else 30 | assert_nil clone[@klass.primary_key], "Sole primary key should be nil" 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_composite_arrays.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | class CompositeArraysTest < ActiveSupport::TestCase 6 | 7 | def test_new_primary_keys 8 | keys = CompositePrimaryKeys::CompositeKeys.new 9 | assert_not_nil keys 10 | assert_equal '', keys.to_s 11 | assert_equal '', "#{keys}" 12 | end 13 | 14 | def test_initialize_primary_keys 15 | keys = CompositePrimaryKeys::CompositeKeys.new([1,2,3]) 16 | assert_not_nil keys 17 | assert_equal '1,2,3', keys.to_s 18 | assert_equal '1,2,3', "#{keys}" 19 | end 20 | 21 | def test_to_composite_keys 22 | keys = [1,2,3].to_composite_keys 23 | assert_equal CompositePrimaryKeys::CompositeKeys, keys.class 24 | assert_equal '1,2,3', keys.to_s 25 | end 26 | 27 | def test_new_ids 28 | keys = CompositePrimaryKeys::CompositeIds.new 29 | assert_not_nil keys 30 | assert_equal '', keys.to_s 31 | assert_equal '', "#{keys}" 32 | end 33 | 34 | def test_initialize_ids 35 | keys = CompositePrimaryKeys::CompositeIds.new([1,2,3]) 36 | assert_not_nil keys 37 | assert_equal '1,2,3', keys.to_s 38 | assert_equal '1,2,3', "#{keys}" 39 | end 40 | 41 | def test_to_composite_ids 42 | keys = [1,2,3].to_composite_ids 43 | assert_equal CompositePrimaryKeys::CompositeIds, keys.class 44 | assert_equal '1,2,3', keys.to_s 45 | end 46 | 47 | def test_flatten 48 | keys = [CompositePrimaryKeys::CompositeIds.new([1,2,3]), CompositePrimaryKeys::CompositeIds.new([4,5,6])] 49 | assert_equal 6, keys.flatten.size 50 | end 51 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_create.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | require 'fixtures/street' 5 | require 'fixtures/suburb' 6 | 7 | class TestCreate < ActiveSupport::TestCase 8 | fixtures :reference_types, :reference_codes, :streets, :suburbs 9 | 10 | CLASSES = { 11 | :single => { 12 | :class => ReferenceType, 13 | :primary_keys => :reference_type_id, 14 | :create => {:reference_type_id => 10, :type_label => 'NEW_TYPE', :abbreviation => 'New Type'} 15 | }, 16 | :dual => { 17 | :class => ReferenceCode, 18 | :primary_keys => [:reference_type_id, :reference_code], 19 | :create => {:reference_type_id => 1, :reference_code => 20, :code_label => 'NEW_CODE', :abbreviation => 'New Code'} 20 | }, 21 | } 22 | 23 | def setup 24 | self.class.classes = CLASSES 25 | end 26 | 27 | def test_setup 28 | testing_with do 29 | assert_not_nil @klass_info[:create] 30 | end 31 | end 32 | 33 | def test_create 34 | testing_with do 35 | assert new_obj = @klass.create(@klass_info[:create]) 36 | assert !new_obj.new_record? 37 | end 38 | end 39 | 40 | def test_create_no_id 41 | testing_with do 42 | begin 43 | @obj = @klass.create(@klass_info[:create].block(@klass.primary_key)) 44 | @successful = !composite? 45 | rescue CompositePrimaryKeys::ActiveRecord::CompositeKeyError 46 | @successful = false 47 | rescue 48 | flunk "Incorrect exception raised: #{$!}, #{$!.class}" 49 | end 50 | assert_equal composite?, !@successful, "Create should have failed for composites; #{@obj.inspect}" 51 | end 52 | end 53 | 54 | def test_create_on_association 55 | suburb = Suburb.find(:first) 56 | suburb.streets.create(:name => "my street") 57 | street = Street.find_by_name('my street') 58 | assert_equal(suburb.city_id, street.city_id) 59 | assert_equal(suburb.suburb_id, street.suburb_id) 60 | end 61 | 62 | def test_create_on_association_when_belongs_to_is_single_key 63 | rt = ReferenceType.find(:first) 64 | rt.reference_codes.create(:reference_code => 4321, :code_label => 'foo', :abbreviation => 'bar') 65 | rc = ReferenceCode.find_by_reference_code(4321) 66 | assert_equal(rc.reference_type_id, rt.reference_type_id) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_delete.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | require 'fixtures/department' 5 | require 'fixtures/employee' 6 | 7 | class TestDelete < ActiveSupport::TestCase 8 | fixtures :reference_types, :reference_codes, :departments, :employees 9 | 10 | CLASSES = { 11 | :single => { 12 | :class => ReferenceType, 13 | :primary_keys => :reference_type_id, 14 | }, 15 | :dual => { 16 | :class => ReferenceCode, 17 | :primary_keys => [:reference_type_id, :reference_code], 18 | }, 19 | } 20 | 21 | def setup 22 | self.class.classes = CLASSES 23 | end 24 | 25 | def test_destroy_one 26 | testing_with do 27 | #assert @first.destroy 28 | assert true 29 | end 30 | end 31 | 32 | def test_destroy_one_via_class 33 | testing_with do 34 | assert @klass.destroy(*@first.id) 35 | end 36 | end 37 | 38 | def test_destroy_one_alone_via_class 39 | testing_with do 40 | assert @klass.destroy(@first.id) 41 | end 42 | end 43 | 44 | def test_delete_one 45 | testing_with do 46 | assert @klass.delete(*@first.id) if composite? 47 | end 48 | end 49 | 50 | def test_delete_one_alone 51 | testing_with do 52 | assert @klass.delete(@first.id) 53 | end 54 | end 55 | 56 | def test_delete_many 57 | testing_with do 58 | to_delete = @klass.find(:all)[0..1] 59 | assert_equal 2, to_delete.length 60 | end 61 | end 62 | 63 | def test_delete_all 64 | testing_with do 65 | @klass.delete_all 66 | end 67 | end 68 | 69 | def test_clear_association 70 | department = Department.find(1,1) 71 | assert_equal 2, department.employees.size, "Before clear employee count should be 2." 72 | department.employees.clear 73 | assert_equal 0, department.employees.size, "After clear employee count should be 0." 74 | department.reload 75 | assert_equal 0, department.employees.size, "After clear and a reload from DB employee count should be 0." 76 | end 77 | 78 | def test_delete_association 79 | department = Department.find(1,1) 80 | assert_equal 2, department.employees.size , "Before delete employee count should be 2." 81 | first_employee = department.employees[0] 82 | department.employees.delete(first_employee) 83 | assert_equal 1, department.employees.size, "After delete employee count should be 1." 84 | department.reload 85 | assert_equal 1, department.employees.size, "After delete and a reload from DB employee count should be 1." 86 | end 87 | 88 | def test_delete_records_for_has_many_association_with_composite_primary_key 89 | reference_type = ReferenceType.find(1) 90 | codes_to_delete = reference_type.reference_codes[0..1] 91 | assert_equal 3, reference_type.reference_codes.size, "Before deleting records reference_code count should be 3." 92 | reference_type.reference_codes.delete_records(codes_to_delete) 93 | reference_type.reload 94 | assert_equal 1, reference_type.reference_codes.size, "After deleting 2 records and a reload from DB reference_code count should be 1." 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_dummy.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | class TestDummy < ActiveSupport::TestCase 6 | fixtures :reference_types, :reference_codes 7 | 8 | classes = { 9 | :single => { 10 | :class => ReferenceType, 11 | :primary_keys => :reference_type_id, 12 | }, 13 | :dual => { 14 | :class => ReferenceCode, 15 | :primary_keys => [:reference_type_id, :reference_code], 16 | }, 17 | } 18 | 19 | def setup 20 | self.class.classes = classes 21 | end 22 | 23 | def test_truth 24 | testing_with do 25 | assert true 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_exists.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/article' 3 | require 'fixtures/department' 4 | 5 | class TestExists < ActiveSupport::TestCase 6 | fixtures :articles, :departments 7 | 8 | def test_single_key_exists_giving_id 9 | assert Article.exists?(1) 10 | end 11 | 12 | def test_single_key_exists_giving_condition 13 | assert Article.exists?(['name = ?', 'Article One']) 14 | end 15 | 16 | def test_composite_key_exists_giving_ids_as_string 17 | assert Department.exists?('1,1,') 18 | end 19 | 20 | def test_composite_key_exists_giving_ids_as_array 21 | assert Department.exists?([1,1]) 22 | assert_equal(false, Department.exists?([1111,1111])) 23 | end 24 | 25 | def test_composite_key_exists_giving_ids_as_condition 26 | assert Department.exists?(['department_id = ? and location_id = ?', 1, 1]) 27 | assert_equal(false, Department.exists?(['department_id = ? and location_id = ?', 11111, 11111])) 28 | end 29 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_find.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | # Testing the find action on composite ActiveRecords with two primary keys 6 | class TestFind < ActiveSupport::TestCase 7 | fixtures :reference_types, :reference_codes 8 | 9 | CLASSES = { 10 | :single => { 11 | :class => ReferenceType, 12 | :primary_keys => [:reference_type_id], 13 | }, 14 | :dual => { 15 | :class => ReferenceCode, 16 | :primary_keys => [:reference_type_id, :reference_code], 17 | }, 18 | :dual_strs => { 19 | :class => ReferenceCode, 20 | :primary_keys => ['reference_type_id', 'reference_code'], 21 | }, 22 | } 23 | 24 | def setup 25 | self.class.classes = CLASSES 26 | end 27 | 28 | def test_find_first 29 | testing_with do 30 | obj = @klass.find(:first) 31 | assert obj 32 | assert_equal @klass, obj.class 33 | end 34 | end 35 | 36 | def test_find 37 | testing_with do 38 | found = @klass.find(*first_id) # e.g. find(1,1) or find 1,1 39 | assert found 40 | assert_equal @klass, found.class 41 | assert_equal found, @klass.find(found.id) 42 | assert_equal found, @klass.find(found.to_param) 43 | end 44 | end 45 | 46 | def test_find_composite_ids 47 | testing_with do 48 | found = @klass.find(first_id) # e.g. find([1,1].to_composite_ids) 49 | assert found 50 | assert_equal @klass, found.class 51 | assert_equal found, @klass.find(found.id) 52 | assert_equal found, @klass.find(found.to_param) 53 | end 54 | end 55 | 56 | def test_to_param 57 | testing_with do 58 | assert_equal first_id_str, @first.to_param.to_s 59 | end 60 | end 61 | 62 | def things_to_look_at 63 | testing_with do 64 | assert_equal found, @klass.find(found.id.to_s) # fails for 2+ keys 65 | end 66 | end 67 | 68 | def test_not_found 69 | assert_raise(::ActiveRecord::RecordNotFound) do 70 | ReferenceCode.send :find, '999,999' 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_ids.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | class TestIds < ActiveSupport::TestCase 6 | fixtures :reference_types, :reference_codes 7 | 8 | CLASSES = { 9 | :single => { 10 | :class => ReferenceType, 11 | :primary_keys => [:reference_type_id], 12 | }, 13 | :dual => { 14 | :class => ReferenceCode, 15 | :primary_keys => [:reference_type_id, :reference_code], 16 | }, 17 | :dual_strs => { 18 | :class => ReferenceCode, 19 | :primary_keys => ['reference_type_id', 'reference_code'], 20 | }, 21 | } 22 | 23 | def setup 24 | self.class.classes = CLASSES 25 | end 26 | 27 | def test_id 28 | testing_with do 29 | assert_equal @first.id, @first.ids if composite? 30 | end 31 | end 32 | 33 | def test_id_to_s 34 | testing_with do 35 | assert_equal first_id_str, @first.id.to_s 36 | assert_equal first_id_str, "#{@first.id}" 37 | end 38 | end 39 | 40 | def test_ids_to_s 41 | testing_with do 42 | order = @klass.primary_key.is_a?(String) ? @klass.primary_key : @klass.primary_key.join(',') 43 | to_test = @klass.find(:all, :order => order)[0..1].map(&:id) 44 | assert_equal '(1,1),(1,2)', @klass.ids_to_s(to_test) if @key_test == :dual 45 | assert_equal '1,1;1,2', @klass.ids_to_s(to_test, ',', ';', '', '') if @key_test == :dual 46 | end 47 | end 48 | 49 | def test_composite_where_clause 50 | testing_with do 51 | where = 'reference_codes.reference_type_id=1 AND reference_codes.reference_code=2) OR (reference_codes.reference_type_id=2 AND reference_codes.reference_code=2' 52 | assert_equal(where, @klass.composite_where_clause([[1, 2], [2, 2]])) if @key_test == :dual 53 | end 54 | end 55 | 56 | def test_set_ids_string 57 | testing_with do 58 | array = @primary_keys.collect {|key| 5} 59 | expected = composite? ? array.to_composite_keys : array.first 60 | @first.id = expected.to_s 61 | assert_equal expected, @first.id 62 | end 63 | end 64 | 65 | def test_set_ids_array 66 | testing_with do 67 | array = @primary_keys.collect {|key| 5} 68 | expected = composite? ? array.to_composite_keys : array.first 69 | @first.id = expected 70 | assert_equal expected, @first.id 71 | end 72 | end 73 | 74 | def test_set_ids_comp 75 | testing_with do 76 | array = @primary_keys.collect {|key| 5} 77 | expected = composite? ? array.to_composite_keys : array.first 78 | @first.id = expected 79 | assert_equal expected, @first.id 80 | end 81 | end 82 | 83 | def test_primary_keys 84 | testing_with do 85 | if composite? 86 | assert_not_nil @klass.primary_keys 87 | assert_equal @primary_keys.map {|key| key.to_sym}, @klass.primary_keys 88 | assert_equal @klass.primary_keys, @klass.primary_key 89 | else 90 | assert_not_nil @klass.primary_key 91 | assert_equal @primary_keys, [@klass.primary_key.to_sym] 92 | end 93 | assert_equal @primary_keys.join(','), @klass.primary_key.to_s 94 | # Need a :primary_keys should be Array with to_s overridden 95 | end 96 | end 97 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_miscellaneous.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | class TestMiscellaneous < ActiveSupport::TestCase 6 | fixtures :reference_types, :reference_codes, :products 7 | 8 | CLASSES = { 9 | :single => { 10 | :class => ReferenceType, 11 | :primary_keys => :reference_type_id, 12 | }, 13 | :dual => { 14 | :class => ReferenceCode, 15 | :primary_keys => [:reference_type_id, :reference_code], 16 | }, 17 | } 18 | 19 | def setup 20 | self.class.classes = CLASSES 21 | end 22 | 23 | def test_composite_class 24 | testing_with do 25 | assert_equal composite?, @klass.composite? 26 | end 27 | end 28 | 29 | def test_composite_instance 30 | testing_with do 31 | assert_equal composite?, @first.composite? 32 | end 33 | end 34 | 35 | def test_count 36 | assert_equal 2, Product.count 37 | end 38 | 39 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_pagination.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | require 'plugins/pagination' 5 | 6 | class TestPagination < ActiveSupport::TestCase 7 | fixtures :reference_types, :reference_codes 8 | 9 | include ActionController::Pagination 10 | DEFAULT_PAGE_SIZE = 2 11 | 12 | attr_accessor :params 13 | 14 | CLASSES = { 15 | :single => { 16 | :class => ReferenceType, 17 | :primary_keys => :reference_type_id, 18 | :table => :reference_types, 19 | }, 20 | :dual => { 21 | :class => ReferenceCode, 22 | :primary_keys => [:reference_type_id, :reference_code], 23 | :table => :reference_codes, 24 | }, 25 | } 26 | 27 | def setup 28 | self.class.classes = CLASSES 29 | @params = {} 30 | end 31 | 32 | def test_paginate_all 33 | testing_with do 34 | @object_pages, @objects = paginate @klass_info[:table], :per_page => DEFAULT_PAGE_SIZE 35 | assert_equal 2, @objects.length, "Each page should have #{DEFAULT_PAGE_SIZE} items" 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_polymorphic.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/comment' 3 | require 'fixtures/user' 4 | require 'fixtures/employee' 5 | require 'fixtures/hack' 6 | 7 | class TestPolymorphic < ActiveSupport::TestCase 8 | fixtures :users, :employees, :comments, :hacks 9 | 10 | def test_polymorphic_has_many 11 | comments = Hack.find('andrew').comments 12 | assert_equal 'andrew', comments[0].person_id 13 | end 14 | 15 | def test_polymorphic_has_one 16 | first_comment = Hack.find('andrew').first_comment 17 | assert_equal 'andrew', first_comment.person_id 18 | end 19 | 20 | def test_has_many_through 21 | user = users(:santiago) 22 | article_names = user.articles.collect { |a| a.name }.sort 23 | assert_equal ['Article One', 'Article Two'], article_names 24 | end 25 | 26 | def test_polymorphic_has_many_through 27 | user = users(:santiago) 28 | assert_equal ['andrew'], user.hacks.collect { |a| a.name }.sort 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_santiago.rb: -------------------------------------------------------------------------------- 1 | # Test cases devised by Santiago that broke the Composite Primary Keys 2 | # code at one point in time. But no more!!! 3 | 4 | require 'abstract_unit' 5 | require 'fixtures/user' 6 | require 'fixtures/article' 7 | require 'fixtures/reading' 8 | 9 | class TestSantiago < ActiveSupport::TestCase 10 | fixtures :suburbs, :streets, :users, :articles, :readings 11 | 12 | def test_normal_and_composite_associations 13 | assert_not_nil @suburb = Suburb.find(1,1) 14 | assert_equal 1, @suburb.streets.length 15 | 16 | assert_not_nil @street = Street.find(1) 17 | assert_not_nil @street.suburb 18 | end 19 | 20 | def test_single_keys 21 | @santiago = User.find(1) 22 | assert_not_nil @santiago.articles 23 | assert_equal 2, @santiago.articles.length 24 | assert_not_nil @santiago.readings 25 | assert_equal 2, @santiago.readings.length 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /composite_primary_keys/test/test_tutorial_examle.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/user' 3 | require 'fixtures/group' 4 | require 'fixtures/membership_status' 5 | require 'fixtures/membership' 6 | 7 | class TestTutorialExample < ActiveSupport::TestCase 8 | fixtures :users, :groups, :memberships, :membership_statuses 9 | 10 | def test_membership 11 | assert(membership = Membership.find(1,1), "Cannot find a membership") 12 | assert(membership.user) 13 | assert(membership.group) 14 | end 15 | 16 | def test_status 17 | assert(membership = Membership.find(1,1), "Cannot find a membership") 18 | assert(statuses = membership.statuses, "No has_many association to status") 19 | assert_equal(membership, statuses.first.membership) 20 | end 21 | 22 | def test_count 23 | assert(membership = Membership.find(1,1), "Cannot find a membership") 24 | assert_equal(1, membership.statuses.count) 25 | end 26 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_update.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/reference_type' 3 | require 'fixtures/reference_code' 4 | 5 | class TestUpdate < ActiveSupport::TestCase 6 | fixtures :reference_types, :reference_codes 7 | 8 | CLASSES = { 9 | :single => { 10 | :class => ReferenceType, 11 | :primary_keys => :reference_type_id, 12 | :update => { :description => 'RT Desc' }, 13 | }, 14 | :dual => { 15 | :class => ReferenceCode, 16 | :primary_keys => [:reference_type_id, :reference_code], 17 | :update => { :description => 'RT Desc' }, 18 | }, 19 | } 20 | 21 | def setup 22 | self.class.classes = CLASSES 23 | end 24 | 25 | def test_setup 26 | testing_with do 27 | assert_not_nil @klass_info[:update] 28 | end 29 | end 30 | 31 | def test_update_attributes 32 | testing_with do 33 | assert @first.update_attributes(@klass_info[:update]) 34 | assert @first.reload 35 | @klass_info[:update].each_pair do |attr_name, new_value| 36 | assert_equal new_value, @first[attr_name], "Attribute #{attr_name} is incorrect" 37 | end 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /composite_primary_keys/test/test_validations.rb: -------------------------------------------------------------------------------- 1 | require 'abstract_unit' 2 | require 'fixtures/seat' 3 | 4 | class TestValidations < ActiveSupport::TestCase 5 | fixtures :seats 6 | 7 | def test_uniqueness_validation_on_saved_record 8 | s = Seat.find([1,1]) 9 | assert s.valid? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /composite_primary_keys/website/index.txt: -------------------------------------------------------------------------------- 1 | h1. Composite Primary Keys 2 | 3 | h1. -> Ruby on Rails 4 | 5 | h1. -> ActiveRecords 6 | 7 | h2. What 8 | 9 | Ruby on Rails does not support composite primary keys. This free software is an extension 10 | to the database layer of Rails - "ActiveRecords":http://wiki.rubyonrails.com/rails/pages/ActiveRecord - to support composite primary keys as transparently as possible. 11 | 12 | Any Ruby script using ActiveRecords can use Composite Primary Keys with this library. 13 | 14 | h2. Installing 15 | 16 |
sudo gem install composite_primary_keys
17 | 18 | Rails: Add the following to the bottom of your environment.rb file 19 | 20 |
require 'composite_primary_keys'
21 | 22 | Ruby scripts: Add the following to the top of your script 23 | 24 |
require 'rubygems'
 25 | require 'composite_primary_keys'
26 | 27 | h2. The basics 28 | 29 | A model with composite primary keys would look like... 30 | 31 |
class Membership < ActiveRecord::Base
 32 |   # set_primary_keys *keys - turns on composite key functionality
 33 |   set_primary_keys :user_id, :group_id
 34 |   belongs_to :user
 35 |   belongs_to :group
 36 |   has_many :statuses, :class_name => 'MembershipStatus', :foreign_key => [:user_id, :group_id]
 37 | end
38 | 39 | A model associated with a composite key model would be defined like... 40 | 41 |
class MembershipStatus < ActiveRecord::Base
 42 |   belongs_to :membership, :foreign_key => [:user_id, :group_id]
 43 | end
44 | 45 | That is, associations can include composite keys too. Nice. 46 | 47 | h2. Demonstration of usage 48 | 49 | Once you've created your models to specify composite primary keys (such as the Membership class) and associations (such as MembershipStatus#membership), you can uses them like any normal model with associations. 50 | 51 | But first, lets check out our primary keys. 52 | 53 |
MembershipStatus.primary_key # => "id"    # normal single key
 54 | Membership.primary_key  # => [:user_id, :group_id] # composite keys
 55 | Membership.primary_key.to_s # => "user_id,group_id"
56 | 57 | Now we want to be able to find instances using the same syntax we always use for ActiveRecords... 58 | 59 |
MembershipStatus.find(1)    # single id returns single instance
 60 | => "1", "status"=>"Active"}>
 61 | Membership.find(1,1)  # composite ids returns single instance
 62 | => "1", "group_id"=>"1"}>
63 | 64 | Using "Ruby on Rails":http://www.rubyonrails.org? You'll want to your url_for helpers 65 | to convert composite keys into strings and back again... 66 | 67 |
Membership.find(:first).to_param # => "1,1"
68 | 69 | And then use the string id within your controller to find the object again 70 | 71 |
params[:id] # => '1,1'
 72 | Membership.find(params[:id])
 73 | => "1", "group_id"=>"1"}>
74 | 75 | That is, an ActiveRecord supporting composite keys behaves transparently 76 | throughout your application. Just like a normal ActiveRecord. 77 | 78 | 79 | h2. Other tricks 80 | 81 | h3. Pass a list of composite ids to the #find method 82 | 83 |
Membership.find [1,1], [2,1]
 84 | => [
 85 |   "1", "group_id"=>"1"}>, 
 86 |   "2", "group_id"=>"1"}>
 87 | ]
88 | 89 | Perform #count operations 90 | 91 |
MembershipStatus.find(:first).memberships.count # => 1
92 | 93 | h3. Routes with Rails 94 | 95 | From Pete Sumskas: 96 | 97 |
98 | I ran into one problem that I didn't see mentioned on "this list":http://groups.google.com/group/compositekeys - 99 | and I didn't see any information about what I should do to address it in the 100 | documentation (might have missed it). 101 | 102 | The problem was that the urls being generated for a 'show' action (for 103 | example) had a syntax like: 104 | 105 |
/controller/show/123000,Bu70
106 | 107 | for a two-field composite PK. The default routing would not match that, 108 | so after working out how to do the routing I added: 109 | 110 |
map.connect ':controller/:action/:id', :id => /\w+(,\w+)*/
111 | 112 | to my route.rb file. 113 | 114 |
115 | 116 | 117 | 118 | h2. Which databases? 119 | 120 | 121 | A suite of unit tests have been run on the following databases supported by ActiveRecord: 122 | 123 | |_.Database|_.Test Success|_.User feedback| 124 | |mysql |YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Mysql+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Mysql+is+failing)| 125 | |sqlite3 |YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+failing)| 126 | |postgresql|YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Postgresql+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Postgresql+is+failing)| 127 | |oracle |YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Oracle+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Oracle+is+failing)| 128 | |sqlserver |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+SQLServer)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=SQLServer+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=SQLServer+is+failing)| 129 | |db2 |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+DB2)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=DB2+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=DB2+is+failing)| 130 | |firebird |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Firebird)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Firebird+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Firebird+is+failing)| 131 | |sybase |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Sybase)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Sybase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Sybase+is+failing)| 132 | |openbase |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Openbase)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Openbase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Openbase+is+failing)| 133 | |frontbase |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Frontbase)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Frontbase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Frontbase+is+failing)| 134 | 135 | h2. Dr Nic's Blog 136 | 137 | "http://www.drnicwilliams.com":http://www.drnicwilliams.com - for future announcements and 138 | other stories and things. 139 | 140 | h2. Forum 141 | 142 | "http://groups.google.com/group/compositekeys":http://groups.google.com/group/compositekeys 143 | 144 | h2. How to submit patches 145 | 146 | Read the "8 steps for fixing other people's code":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/ and for section "8b: Submit patch to Google Groups":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/#8b-google-groups, use the Google Group above. 147 | 148 | 149 | The source for this project is available via git. You can "browse and/or fork the source":http://github.com/drnic/composite_primary_keys/tree/master, or to clone the project locally: 150 | 151 |
git clone git://github.com/drnic/composite_primary_keys.git
152 | 153 | h2. Licence 154 | 155 | This code is free to use under the terms of the MIT licence. 156 | 157 | h2. Contact 158 | 159 | Comments are welcome. Send an email to "Dr Nic Williams":mailto:drnicwilliams@gmail.com. 160 | -------------------------------------------------------------------------------- /composite_primary_keys/website/stylesheets/screen.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #2F30EE; 3 | font-family: "Georgia", sans-serif; 4 | font-size: 16px; 5 | line-height: 1.6em; 6 | padding: 1.6em 0 0 0; 7 | color: #eee; 8 | } 9 | h1, h2, h3, h4, h5, h6 { 10 | color: #FFEDFA; 11 | } 12 | h1 { 13 | font-family: sans-serif; 14 | font-weight: normal; 15 | font-size: 4em; 16 | line-height: 0.8em; 17 | letter-spacing: -0.1ex; 18 | margin: 5px; 19 | } 20 | li { 21 | padding: 0; 22 | margin: 0; 23 | list-style-type: square; 24 | } 25 | a { 26 | color: #99f; 27 | font-weight: normal; 28 | text-decoration: underline; 29 | } 30 | blockquote { 31 | font-size: 90%; 32 | font-style: italic; 33 | border-left: 1px solid #eee; 34 | padding-left: 1em; 35 | } 36 | .caps { 37 | font-size: 80%; 38 | } 39 | 40 | #main { 41 | width: 45em; 42 | padding: 0; 43 | margin: 0 auto; 44 | } 45 | .coda { 46 | text-align: right; 47 | color: #77f; 48 | font-size: smaller; 49 | } 50 | 51 | table { 52 | font-size: 90%; 53 | line-height: 1.4em; 54 | color: #ff8; 55 | background-color: #111; 56 | padding: 2px 10px 2px 10px; 57 | border-style: dashed; 58 | } 59 | 60 | th { 61 | color: #fff; 62 | } 63 | 64 | td { 65 | padding: 2px 10px 2px 10px; 66 | } 67 | 68 | .success { 69 | color: #0CC52B; 70 | } 71 | 72 | .failed { 73 | color: #E90A1B; 74 | } 75 | 76 | .unknown { 77 | color: #995000; 78 | } 79 | pre, code { 80 | font-family: monospace; 81 | font-size: 90%; 82 | line-height: 1.4em; 83 | color: #ff8; 84 | background-color: #111; 85 | padding: 2px 10px 2px 10px; 86 | } 87 | .comment { color: #aaa; font-style: italic; } 88 | .keyword { color: #eff; font-weight: bold; } 89 | .punct { color: #eee; font-weight: bold; } 90 | .symbol { color: #0bb; } 91 | .string { color: #6b4; } 92 | .ident { color: #ff8; } 93 | .constant { color: #66f; } 94 | .regex { color: #ec6; } 95 | .number { color: #F99; } 96 | .expr { color: #227; } 97 | 98 | #version { 99 | float: right; 100 | text-align: right; 101 | font-family: sans-serif; 102 | font-weight: normal; 103 | background-color: #ff8; 104 | color: #66f; 105 | padding: 15px 20px 10px 20px; 106 | margin: 0 auto; 107 | margin-top: 15px; 108 | border: 3px solid #66f; 109 | } 110 | 111 | #version .numbers { 112 | display: block; 113 | font-size: 4em; 114 | line-height: 0.8em; 115 | letter-spacing: -0.1ex; 116 | } 117 | 118 | #version a { 119 | text-decoration: none; 120 | } 121 | 122 | .clickable { 123 | cursor: pointer; 124 | cursor: hand; 125 | } 126 | 127 | -------------------------------------------------------------------------------- /composite_primary_keys/website/template.js: -------------------------------------------------------------------------------- 1 | // <%= title %> 2 | var version = <%= version.to_json %>; 3 | <%= body %> 4 | -------------------------------------------------------------------------------- /composite_primary_keys/website/template.rhtml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | <%= title %> 9 | 10 | 11 | 14 | 29 | 30 | 31 |
32 | 33 |

<%= title %>

34 |
35 | Get Version 36 | <%= version %> 37 |
38 | <%= body %> 39 |

40 | Dr Nic, <%= modified.pretty %>
41 | Theme extended from Paul Battley 42 |

43 |
44 | 45 | 47 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /composite_primary_keys/website/version-raw.js: -------------------------------------------------------------------------------- 1 | // Announcement JS file 2 | var version = "2.3.5.1"; 3 | MagicAnnouncement.show('compositekeys', version); 4 | -------------------------------------------------------------------------------- /composite_primary_keys/website/version-raw.txt: -------------------------------------------------------------------------------- 1 | h1. Announcement JS file 2 | MagicAnnouncement.show('compositekeys', version); -------------------------------------------------------------------------------- /composite_primary_keys/website/version.js: -------------------------------------------------------------------------------- 1 | // Version JS file 2 | var version = "2.3.5.1"; 3 | 4 | document.write(" - " + version); 5 | -------------------------------------------------------------------------------- /composite_primary_keys/website/version.txt: -------------------------------------------------------------------------------- 1 | h1. Version JS file 2 | 3 | document.write(" - " + version); -------------------------------------------------------------------------------- /mysql_to_postgresql: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | gem 'activerecord' 5 | gem 'activesupport' 6 | require 'active_record' 7 | require 'active_record/base' 8 | require 'active_support' 9 | 10 | require File.dirname(__FILE__) + '/composite_primary_keys/lib/composite_primary_keys' 11 | 12 | ActiveRecord::Base.logger = Logger.new("mysql_to_postgresql.log") 13 | 14 | TABLES_WITHOUT_SEQUENCES = ["schema_migrations"] 15 | TABLES_WITHOUT_UPDATED_AT = TABLES_WITHOUT_SEQUENCES + [] 16 | 17 | PROCESSES = 8 18 | 19 | pids = [] 20 | PROCESSES.times do |process_index| 21 | pids << fork do 22 | class MysqlModelBase < ActiveRecord::Base 23 | establish_connection( 24 | :adapter => "mysql", 25 | :username => "username", 26 | :password => "password", 27 | :database => "database", 28 | :encoding => "UTF8" 29 | ) 30 | end 31 | 32 | class PostgresqlModelBase < ActiveRecord::Base 33 | establish_connection( 34 | :adapter => "postgresql", 35 | :username => "username", 36 | :password => "password", 37 | :database => "database", 38 | :encoding => "UTF8" 39 | ) 40 | end 41 | 42 | MysqlModelBase.connection.tables.each_with_index do |table, table_index| 43 | next unless table_index % PROCESSES == process_index 44 | puts "[#{process_index}] Starting table: #{table}" 45 | PostgresqlModelBase.transaction do 46 | mysql_model_class = Class.new(MysqlModelBase) do 47 | set_table_name table 48 | end 49 | 50 | id_column = case table 51 | when "schema_migrations" 52 | "version" 53 | else 54 | "id" 55 | end 56 | 57 | postgresql_model_class = Class.new(PostgresqlModelBase) do 58 | set_table_name table 59 | set_primary_keys(id_column) 60 | end 61 | 62 | # Clear out tables without updated_at since we cannot easily figure out what has changed 63 | # Remove updated rows for tables with primary keys (and updated_at columns) 64 | if TABLES_WITHOUT_UPDATED_AT.include?(table) 65 | puts "[#{process_index}] - #{table} - clearing out all rows" 66 | postgresql_model_class.delete_all 67 | else 68 | last_updated_at = PostgresqlModelBase.connection.select_value("select max(updated_at) from #{table}") 69 | updated_mysql_ids = MysqlModelBase.connection.select_values("select #{id_column} from #{table} where updated_at >= '#{last_updated_at}'") 70 | if updated_mysql_ids.size > 0 71 | puts "[#{process_index}] - #{table} - clearing out #{updated_mysql_ids.size} rows updated since last run" 72 | PostgresqlModelBase.connection.execute("delete from #{table} where #{id_column} in (#{updated_mysql_ids.join(",")})") 73 | end 74 | end 75 | 76 | mysql_ids = MysqlModelBase.connection.select_values("select #{id_column} from #{table}") 77 | postgresql_ids = PostgresqlModelBase.connection.select_values("select #{id_column} from #{table}") 78 | 79 | # Remove deleted rows 80 | deleted_record_ids = postgresql_ids - mysql_ids 81 | puts "[#{process_index}] - #{table} - clearing out #{deleted_record_ids.size} deleted rows" 82 | unless TABLES_WITHOUT_UPDATED_AT.include?(table) 83 | PostgresqlModelBase.connection.execute("delete from #{table} where #{id_column} in (#{deleted_record_ids.join(",")})") unless deleted_record_ids.empty? 84 | end 85 | 86 | # Add missing rows 87 | new_record_ids = mysql_ids - postgresql_ids 88 | puts "[#{process_index}] - #{table} - adding missing rows" 89 | start, index = Time.now, 0 90 | new_record_ids.in_groups_of(1000) do |ids| 91 | puts "[#{process_index}] -- #{table} - #{index * 1000}/#{new_record_ids.size} #{Time.now - start} sec" if index % 10 == 0 92 | index +=1 93 | mysql_models = mysql_model_class.find(:all, :conditions => {id_column => ids}) 94 | mysql_models.each {|mysql_model| postgresql_model_class.create!(mysql_model.attributes) } 95 | end 96 | 97 | # Set sequence values 98 | unless TABLES_WITHOUT_SEQUENCES.include?(table) 99 | PostgresqlModelBase.connection.execute("select setval('#{table}_#{id_column}_seq', (select max(#{id_column}) from #{table}))") 100 | end 101 | end 102 | end 103 | end 104 | end 105 | pids.each { |pid| Process.waitpid(pid) } 106 | --------------------------------------------------------------------------------