├── README.textile ├── Rakefile ├── init.rb ├── install.rb ├── lib ├── rails_on_pg.rb └── rails_on_pg │ ├── foreign_keys.rb │ ├── functions.rb │ └── views.rb ├── test ├── foreign_keys_test.rb ├── functions_test.rb ├── schema.rb ├── test_helper.rb └── views_test.rb └── uninstall.rb /README.textile: -------------------------------------------------------------------------------- 1 | h1. RailsOnPG 2 | 3 | This is timesaver for middle/large Rails application which used PostgreSQL as database. 4 | Create/drop Views, Functions, Triggers, Foreign keys in your migrations using ruby syntax. 5 | 6 | h2. Installation 7 | 8 |
 9 |   script/plugin install git://github.com/alex3t/rails_on_pg.git
10 | 
11 | 12 | h2. Views 13 | 14 |
15 |   create_view :active_patients do |view|
16 |     view.select 'p.patient_id as id' ,'p.id as visit_id'
17 |     view.from 'patients as p'
18 |     view.join 'left join demographics d on d.visit_id=v.id'
19 |     view.conditions 'p.status'=>'active','p.name' => 'John' #or "p.status='active' and p.name='John'"
20 |   end
21 | 
22 | 23 | h2. Functions 24 | 25 |
26 |   create_function 'format_name', {:returns=>'character varying'}, 'first_name character varying(125)', 'middle_name character varying(15)', "last_name character varying(20)" do
27 |     "RETURN COALESCE(last_name, 'no last name') || ', ' || COALESCE(first_name, 'no first name');"
28 |   end  
29 | 
30 | 31 | h2. Triggers 32 | 33 |
34 |   create_trigger "update_status", :before, "users", "insert","update"
35 |     #update status function body here
36 |   end
37 | 
38 | 39 | h2. Foreign keys 40 | 41 |
42 |   add_foreign_key :order_items, :product_id, :orders, :on_delete=>""
43 | 
44 | 45 | For more details see rdoc or tests 46 | 47 | 48 | h3. Todo 49 | 50 | Make as gem 51 | 52 | 53 | Copyright (c) 2009 Alex Tretyakov, released under MIT license -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the rails_on_pg plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.libs << 'test' 12 | t.pattern = 'test/**/*_test.rb' 13 | t.verbose = true 14 | end 15 | 16 | desc 'Generate documentation for the rails_on_pg plugin.' 17 | Rake::RDocTask.new(:rdoc) do |rdoc| 18 | rdoc.rdoc_dir = 'rdoc' 19 | rdoc.title = 'rails_on_pg' 20 | rdoc.options << '--line-numbers' << '--inline-source' 21 | rdoc.rdoc_files.include('README') 22 | rdoc.rdoc_files.include('lib/**/*.rb') 23 | end 24 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'rails_on_pg' 2 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex3t/rails_on_pg/026cb1b6bef2dc637d65b504081d3cd2faeda117/install.rb -------------------------------------------------------------------------------- /lib/rails_on_pg.rb: -------------------------------------------------------------------------------- 1 | 2 | require "rails_on_pg/foreign_keys" 3 | require "rails_on_pg/views" 4 | require "rails_on_pg/functions" 5 | 6 | ActiveRecord::Migration.send(:extend, RailsOnPg::ForeignKeys) 7 | ActiveRecord::Migration.send(:extend, RailsOnPg::Views) 8 | ActiveRecord::Migration.send(:extend, RailsOnPg::Functions) 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/rails_on_pg/foreign_keys.rb: -------------------------------------------------------------------------------- 1 | module RailsOnPg 2 | module ForeignKeys 3 | 4 | # Define new foreign key 5 | # Example: 6 | # add_foreign_key :order_items,:product_id,:orders 7 | # where orders is orders is referenced table and order_items is referencing table 8 | # foreign key will be: 'fk_order_items_product_id' 9 | # optional options: 10 | # :on_delete 11 | # :on_update 12 | # :column 13 | def add_foreign_key(from_table, from_column, to_table, options={}) 14 | # default delete and on update actions 15 | options.reverse_merge!({:on_delete=>'NO ACTION', :on_update=>'NO ACTION',:column=>'id'}) 16 | constraint_name = "fk_#{from_table}_#{from_column}" 17 | 18 | execute %{ALTER TABLE #{from_table} 19 | ADD CONSTRAINT #{constraint_name} 20 | FOREIGN KEY (#{from_column}) 21 | REFERENCES #{to_table}(#{options[:column]}) 22 | ON UPDATE #{options[:on_update]} 23 | ON DELETE #{options[:on_delete]} } 24 | end 25 | 26 | # Remove prev created key 27 | def remove_foreign_key(from_table, from_column, to_table) 28 | constraint_name = "fk_#{from_table}_#{from_column}" 29 | # check if constraint already exist 30 | count = ActiveRecord::Base.connection.select_value("select count(1) from pg_constraint where conname='#{constraint_name}'") 31 | 32 | unless count.to_i == 0 33 | execute %{ALTER TABLE #{from_table} DROP CONSTRAINT #{constraint_name}} 34 | end 35 | end 36 | 37 | end 38 | end -------------------------------------------------------------------------------- /lib/rails_on_pg/functions.rb: -------------------------------------------------------------------------------- 1 | module RailsOnPg 2 | module Functions 3 | 4 | # Create new plpgsql function 5 | # Example: 6 | # create_function 'format_name', {:returns=>'character varying'}, 'first_name character varying(125)', 'middle_name character varying(15)', "last_name character varying(20)" do 7 | # "RETURN COALESCE(last_name, 'no last name') || ', ' || COALESCE(first_name, 'no first name') || ' ' || COALESCE(middle_name || '.','');" 8 | # end 9 | def create_function name, options={}, *params 10 | options.reverse_merge!({:as=>'$$'}) 11 | returns = options[:returns] 12 | declare = %{DECLARE 13 | #{options[:declare].join(';')}} if options[:declare] 14 | drop_function name, params 15 | # execute 16 | set_lang 17 | execute %{CREATE FUNCTION #{name}(#{params.join(',')}) RETURNS #{returns} AS #{options[:as]} 18 | #{declare} 19 | BEGIN 20 | #{yield} 21 | END; 22 | #{options[:as]} LANGUAGE 'plpgsql'; 23 | } 24 | end 25 | 26 | # drop function 27 | def drop_function name, *params 28 | execute "DROP FUNCTION IF EXISTS #{name}(#{params.join(',')}) CASCADE" 29 | end 30 | 31 | # Create trigger function for it 32 | # name - trigger name 33 | # type - :before or :after 34 | # table_name - table name 35 | # actions - "insert","update","delete" 36 | # Example: 37 | # create_trigger "some_tr", :before, "users", "insert","update" 38 | def create_trigger name, type, table_name, *actions 39 | create_function "#{name}_f", :returns=>'trigger',:as=>'$BODY$' do 40 | yield 41 | end 42 | execute %{CREATE TRIGGER #{name} #{type.to_s.upcase} #{actions.map{|str|str.upcase}.join(' OR ')} 43 | ON "#{table_name}" FOR EACH ROW 44 | EXECUTE PROCEDURE #{name}_f();} 45 | end 46 | 47 | # Drop trigger 48 | def drop_trigger name, table_name 49 | execute "DROP TRIGGER #{name} on #{table_name} CASCADE" 50 | end 51 | 52 | private 53 | def set_lang lang='plpgsql' 54 | begin 55 | execute("CREATE LANGUAGE #{lang}") 56 | rescue ActiveRecord::StatementInvalid => ex 57 | end 58 | end 59 | 60 | 61 | end 62 | end -------------------------------------------------------------------------------- /lib/rails_on_pg/views.rb: -------------------------------------------------------------------------------- 1 | module RailsOnPg 2 | module Views 3 | 4 | # Create new view 5 | # name - name of view 6 | # Example: 7 | # create_view :active_patients do |v| 8 | # v.select 'p.patient_id as id' ,'p.id as visit_id' 9 | # v.from 'patients as p' 10 | # v.join 'left join demographics d on d.visit_id=v.id' 11 | # v.join 'left join diagnoses di on di.visit_id=v.id and di.row_index=0' 12 | # v.conditions 'p.status'=>'active','p.name' => 'John' #or "p.status='active' and p.name='John'" 13 | # end 14 | # See ViewDefinition class 15 | def create_view name, options={}, &block 16 | view_def = ViewDefinition.new name, &block 17 | 18 | drop_views name, options[:dependent_views] 19 | execute view_def.to_sql 20 | end 21 | 22 | # Update view's select columns 23 | # name - name of existed view 24 | # type - type of action(:add,:remove or :replace) 25 | # columns - array of columns or string 26 | # options - options 27 | # Options: 28 | # :dependent_views - if view has dependent views(views where current view used) then you need list them here 29 | # Example: 30 | # update_view :active_patients, :add, ['p.first_name as name','p.age as dob'] 31 | # update_view :active_patients, :add, 'p.first_name as name', :dependent_views=>['view0','view1'] 32 | # update_view :active_patients, :remove, 'p.first_name as name', :dependent_views=>['view0','view1'] 33 | # update_view :active_patients, :replace, ['p.first_name as name','p.age as dob'] #replace all select columns 34 | def update_view name, type, columns, options={} 35 | view_structure = ActiveRecord::Base.connection.select_value("select definition from pg_views where viewname='#{name}'") 36 | raise ViewNotExistException("View #{name} does not exist in current db") unless view_structure 37 | 38 | columns_str = columns.is_a?(Array) ? columns.join(',') : columns 39 | 40 | select_pattern = /select (.*) from/i 41 | select_str = view_structure[select_pattern,1] 42 | 43 | case type 44 | when :add 45 | view_structure.gsub!(select_pattern, "SELECT #{select_str}, #{columns_str} FROM") 46 | when :remove 47 | select_str.gsub!(", #{columns_str}", '') 48 | view_structure.gsub!(select_pattern, "SELECT #{select_str} FROM") 49 | when :replace 50 | view_structure.gsub!(select_pattern, "SELECT #{columns_str} FROM") 51 | end 52 | 53 | drop_views name, options[:dependent_views] 54 | execute "CREATE VIEW #{name} AS #{view_structure};" 55 | end 56 | 57 | # drop dependent views before if exists 58 | # Options 59 | # :dependent_views - if view has dependent views(views where current view used) then you need list them here 60 | def drop_views name, defs=nil 61 | defs = defs.delete(:dependent_views) if defs.is_a?(Hash) 62 | defs.each do |dependent_view| 63 | execute "DROP VIEW IF EXISTS #{dependent_view}" 64 | end if defs 65 | 66 | execute "DROP VIEW IF EXISTS #{name}" 67 | 68 | end 69 | 70 | # recreate view without changes 71 | def recreate_view name 72 | view_structure = ActiveRecord::Base.connection.select_value("select definition from pg_views where viewname='#{name}'") 73 | if view_structure 74 | execute "DROP VIEW IF EXISTS #{name}" 75 | execute "CREATE VIEW #{name} AS #{view_structure};" 76 | end 77 | end 78 | 79 | # =========== 80 | # = Classes = 81 | # =========== 82 | class ViewNotExistException < Exception; end 83 | 84 | # View definition, see create_view dsl 85 | class ViewDefinition 86 | def initialize name, &block 87 | @joins = [] 88 | @name = name 89 | instance_eval &block 90 | end 91 | 92 | def select *columns 93 | @select = columns.join(',') 94 | end 95 | def from *tables 96 | @from = tables.join(',') 97 | end 98 | def join value 99 | @joins << value 100 | end 101 | def conditions cond 102 | 103 | @where = cond.collect{ |attrib, value| "#{attrib} = #{value}"}.join(" AND ") if cond.is_a?(Hash) 104 | @where = cond if cond.is_a?(String) 105 | end 106 | 107 | def to_sql 108 | @where ||= '1=1' 109 | "CREATE VIEW #{@name} AS SELECT #{@select} FROM #{@from} #{@joins.join(' ')} WHERE #{@where};" 110 | end 111 | end 112 | 113 | 114 | end 115 | end -------------------------------------------------------------------------------- /test/foreign_keys_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | 4 | 5 | class ForeignKeysTest < ActiveSupport::TestCase 6 | # Replace this with your real tests. 7 | test "should create new foreign key" do 8 | MigrationTest.remove_foreign_key :projects, :user_id, :users 9 | MigrationTest.add_foreign_key :projects, :user_id, :users 10 | fk_name = 'fk_projects_user_id' 11 | count = ActiveRecord::Base.connection.select_value("select count(1) from pg_constraint where conname='#{fk_name}'") 12 | assert_equal 1, count.to_i 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/functions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FunctionsTest < ActiveSupport::TestCase 4 | def setup 5 | 6 | 7 | end 8 | 9 | test "should create new function" do 10 | MigrationTest.create_function 'format_name', {:returns=>'character varying'}, 'first_name character varying(125)', 'middle_name character varying(15)', "last_name character varying(20)" do 11 | "RETURN COALESCE(last_name, 'no last name') || ', ' || COALESCE(first_name, 'no first name') || ' ' || COALESCE(middle_name || '.','');" 12 | end 13 | f_count = ActiveRecord::Base.connection.select_value( 14 | "select count(1) from pg_proc where proname='format_name'") 15 | assert_equal 1, f_count.to_i 16 | end 17 | 18 | test "should succefully drop function" do 19 | MigrationTest.drop_function 'format_name', 'first_name character varying(125)', 'middle_name character varying(15)', "last_name character varying(20)" 20 | f_count = ActiveRecord::Base.connection.select_value( 21 | "select count(1) from pg_proc where proname='format_name'") 22 | assert_equal 0, f_count.to_i 23 | end 24 | 25 | test "should create new trigger" do 26 | MigrationTest.create_trigger "some_tr", :before, "users", "insert","update" do 27 | 28 | end 29 | f_count = ActiveRecord::Base.connection.select_value( 30 | "select count(1) from pg_trigger where tgname='some_tr'") 31 | assert_equal 1, f_count.to_i 32 | end 33 | 34 | test "should succefully drop trigger" do 35 | MigrationTest.drop_trigger 'some_tr','users' 36 | f_count = ActiveRecord::Base.connection.select_value( 37 | "select count(1) from pg_trigger where tgname='some_tr'") 38 | assert_equal 0, f_count.to_i 39 | end 40 | 41 | 42 | 43 | end -------------------------------------------------------------------------------- /test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 0) do 2 | 3 | create_table "projects", :force => true do |t| 4 | t.string "name" 5 | t.integer "user_id" 6 | t.datetime "created_at" 7 | t.datetime "updated_at" 8 | end 9 | 10 | create_table "tasks", :force => true do |t| 11 | t.string "value" 12 | t.integer "project_id" 13 | t.datetime "created_at" 14 | t.datetime "updated_at" 15 | end 16 | 17 | create_table "users", :force => true do |t| 18 | t.string "login" 19 | t.string "email" 20 | t.text "about" 21 | t.boolean "is_active" 22 | t.datetime "created_at" 23 | t.datetime "updated_at" 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'active_support' 3 | require 'active_support/test_case' 4 | require 'active_record' 5 | require 'test/unit' 6 | 7 | 8 | # ENV['RAILS_ENV'] = 'test' 9 | ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..' 10 | 11 | require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb')) 12 | 13 | 14 | 15 | # config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) 16 | ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") 17 | 18 | 19 | # ActiveRecord::Base.establish_connection(config['test']) 20 | 21 | ActiveRecord::Base.establish_connection({ 22 | :adapter => 'postgresql', 23 | :database => 'test2', 24 | :username => 'postgres', 25 | :password => 'postgres', 26 | :host => 'localhost', 27 | }) 28 | 29 | 30 | 31 | load(File.dirname(__FILE__) + "/schema.rb") 32 | 33 | class MigrationTest < ActiveRecord::Migration 34 | end -------------------------------------------------------------------------------- /test/views_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ViewsTest < ActiveSupport::TestCase 4 | def setup 5 | 6 | MigrationTest.create_view 'active_users' do |v| 7 | v.select 'u.id','u.login' 8 | v.from 'users as u' 9 | v.conditions 'is_active'=>'true' 10 | end 11 | end 12 | 13 | 14 | test "create view" do 15 | 16 | def_expected = %{SELECT u.id, u.login FROM users u WHERE (u.is_active = true);} 17 | def_created = ActiveRecord::Base.connection.select_value("select definition from pg_views where viewname='active_users'") 18 | assert_equal def_expected, def_created 19 | MigrationTest.drop_views 'active_users' 20 | end 21 | 22 | test "add column to view" do 23 | 24 | MigrationTest.update_view 'active_users', :add, 'u.email', :dependent_views=>[:active_test_users] 25 | def_expected = %{SELECT u.id, u.login, u.email FROM users u WHERE (u.is_active = true);} 26 | def_created = ActiveRecord::Base.connection.select_value("select definition from pg_views where viewname='active_users'") 27 | assert_equal def_expected, def_created 28 | MigrationTest.drop_views 'active_users' 29 | end 30 | test "remove column to view" do 31 | 32 | MigrationTest.update_view 'active_users', :remove, 'u.login', :dependent_views=>[:active_test_users] 33 | def_expected = %{SELECT u.id FROM users u WHERE (u.is_active = true);} 34 | def_created = ActiveRecord::Base.connection.select_value("select definition from pg_views where viewname='active_users'") 35 | assert_equal def_expected, def_created 36 | MigrationTest.drop_views 'active_users' 37 | end 38 | test "replace columns" do 39 | 40 | MigrationTest.update_view 'active_users', :replace, 'u.login,u.email', :dependent_views=>[:active_test_users] 41 | def_expected = %{SELECT u.login, u.email FROM users u WHERE (u.is_active = true);} 42 | def_created = ActiveRecord::Base.connection.select_value("select definition from pg_views where viewname='active_users'") 43 | assert_equal def_expected, def_created 44 | MigrationTest.drop_views 'active_users' 45 | end 46 | 47 | test "drop dependent views" do 48 | MigrationTest.create_view 'active_test_users' do |v| 49 | v.select '*' 50 | v.from 'active_users' 51 | v.conditions "active_users.login like '%test%'" 52 | end 53 | MigrationTest.drop_views 'active_users', :dependent_views=>[:active_test_users] 54 | count_active_test_users = ActiveRecord::Base.connection.select_value("select count(1) from pg_views where viewname='active_test_users'") 55 | assert_equal 0, count_active_test_users.to_i 56 | count_active_users = ActiveRecord::Base.connection.select_value("select count(1) from pg_views where viewname='active_users'") 57 | assert_equal 0, count_active_users.to_i 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /uninstall.rb: -------------------------------------------------------------------------------- 1 | # Uninstall hook code here 2 | --------------------------------------------------------------------------------