schema_plus_pg_indexes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1c7a0890c1692f1bccb36a22dd4c68c81385e2fa
4
+ data.tar.gz: 6aefdddf74cca2fd8f72120b2098b674a69ae97d
5
+ SHA512:
6
+ metadata.gz: b4418c479638217697120b8278121e7342da4eb4595c2af12807631f9ea5c60bed98645aeb02fe83973865a707159c10d2c3dbf38702642eeb06bab5eb4968bf
7
+ data.tar.gz: fb4644fab3314297ad7ed94487006156630009d395cde790057a00c3818f942193a638de7dd4d53caf0cb77705d6d60367bb4a5b69f7a3aa9b73b583be6ab4ee
@@ -0,0 +1,8 @@
1
+ /coverage
2
+ /tmp
3
+ /pkg
4
+
5
+ *.lock
6
+ *.log
7
+ *.sqlite3
8
+ !gemfiles/**/*.sqlite3
@@ -0,0 +1,17 @@
1
+ # This file was auto-generated by the schema_dev tool, based on the data in
2
+ # ./schema_dev.yml
3
+ # Please do not edit this file; any changes will be overwritten next time
4
+ # schema_dev gets run.
5
+ ---
6
+ sudo: false
7
+ rvm:
8
+ - 1.9.3
9
+ - 2.1.5
10
+ gemfile:
11
+ - gemfiles/rails-4.2/Gemfile.postgresql
12
+ env: POSTGRESQL_DB_USER=postgres
13
+ addons:
14
+ postgresql: '9.3'
15
+ before_script: bundle exec rake create_databases
16
+ after_script: bundle exec rake drop_databases
17
+ script: bundle exec rake travis
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 ronen barzel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,76 @@
1
+ [![Gem Version](https://badge.fury.io/rb/schema_plus_pg_indexes.svg)](http://badge.fury.io/rb/schema_plus_pg_indexes)
2
+ [![Build Status](https://secure.travis-ci.org/SchemaPlus/schema_plus_pg_indexes.svg)](http://travis-ci.org/SchemaPlus/schema_plus_pg_indexes)
3
+ [![Coverage Status](https://img.shields.io/coveralls/SchemaPlus/schema_plus_pg_indexes.svg)](https://coveralls.io/r/SchemaPlus/schema_plus_pg_indexes)
4
+ [![Dependency Status](https://gemnasium.com/lomba/schema_plus_pg_indexes.svg)](https://gemnasium.com/SchemaPlus/schema_plus_pg_indexes)
5
+
6
+ # schema_plus_pg_indexes
7
+
8
+ Schema_plus_pg_indexes adds into `ActiveRecord` support for some additional PostgreSQL index features: expressions, operator classes, and case-insensitive indexes:
9
+
10
+ t.string :last_name, index: { expression: 'upper(last_name)' }
11
+ t.string :last_name, index: { operator_class: 'varchar_pattern_ops' }
12
+ t.string :last_name, index: { with: :address, operator_class: {last_name: 'varchar_pattern_ops', address: 'text_pattern_ops' }
13
+ t.string :last_name, index: { case_sensitive: false }
14
+
15
+ t.index expression: 'upper(last_name)', name: 'my_index' # no column given, must give a name
16
+
17
+ Case insensitivity is a shorthand for the expression `lower(last_name)`
18
+
19
+ The `ActiveRecord::ConnectionAdapters::IndexDefinition` object has the corresponding methods defined on it: `#expression`, `#operator_classes` and `#case_sensitive?`
20
+
21
+ Schema_plus_pg_indexes is part of the [SchemaPlus](https://github.com/SchemaPlus/) family of Ruby on Rails extension gems.
22
+
23
+ ## Installation
24
+
25
+ In your application's Gemfile
26
+
27
+ ```ruby
28
+ gem "schema_plus_pg_indexes"
29
+ ```
30
+ ## Compatibility
31
+
32
+ schema_plus_pg_indexes is tested on
33
+
34
+ [//]: # SCHEMA_DEV: MATRIX - begin
35
+ [//]: # These lines are auto-generated by schema_dev based on schema_dev.yml
36
+ * ruby **1.9.3** with rails **4.2**, using **postgresql**
37
+ * ruby **2.1.5** with rails **4.2**, using **postgresql**
38
+
39
+ [//]: # SCHEMA_DEV: MATRIX - end
40
+
41
+
42
+ ## History
43
+
44
+ ### v0.1.0
45
+
46
+ * Initial release
47
+
48
+ ## Development & Testing
49
+
50
+ Are you interested in contributing to schema_plus_pg_indexes? Thanks! Please follow
51
+ the standard protocol: fork, feature branch, develop, push, and issue pull request.
52
+
53
+ Some things to know about to help you develop and test:
54
+
55
+ * **schema_dev**: schema_plus_pg_indexes uses [schema_dev](https://github.com/SchemaPlus/schema_dev) to
56
+ facilitate running rspec tests on the matrix of ruby, rails, and database
57
+ versions that the gem supports, both locally and on
58
+ [travis-ci](http://travis-ci.org/SchemaPlus/schema_plus_pg_indexes)
59
+
60
+ To to run rspec locally on the full matrix, do:
61
+
62
+ $ schema_dev bundle install
63
+ $ schema_dev rspec
64
+
65
+ You can also run on just one configuration at a time; For info, see `schema_dev --help` or the
66
+ [schema_dev](https://github.com/SchemaPlus/schema_dev) README.
67
+
68
+ The matrix of configurations is specified in `schema_dev.yml` in
69
+ the project root.
70
+
71
+ * **schema_monkey**: schema_plus_pg_indexes extends ActiveRecord using
72
+ [schema_monkey](https://github.com/SchemaPlus/schema_monkey)'s extension
73
+ API and protocols -- see its README for details. If your contribution needs any additional monkey patching
74
+ that isn't already supported by
75
+ [schema_monkey](https://github.com/SchemaPlus/schema_monkey), please head
76
+ over there and submit a PR.
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'schema_dev/tasks'
5
+
6
+ task :default => :spec
7
+
8
+ require 'rspec/core/rake_task'
9
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+ gemspec :path => File.expand_path('..', __FILE__)
3
+
4
+ platform :ruby do
5
+ gem "byebug" if RUBY_VERSION > "2"
6
+ end
7
+
8
+ File.exist?(gemfile_local = File.expand_path('../Gemfile.local', __FILE__)) and eval File.read(gemfile_local), binding, gemfile_local
@@ -0,0 +1,3 @@
1
+ eval File.read File.expand_path('../../Gemfile.base', __FILE__)
2
+
3
+ gem "rails", "~> 4.2.0"
@@ -0,0 +1,10 @@
1
+ require "pathname"
2
+ eval(Pathname.new(__FILE__).dirname.join("Gemfile.base").read, binding)
3
+
4
+ platform :ruby do
5
+ gem "pg"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'schema_monkey'
2
+ require 'schema_plus_indexes'
3
+
4
+ require_relative 'schema_plus_pg_indexes/active_record/connection_adapters/index_definition'
5
+ require_relative 'schema_plus_pg_indexes/middleware/migration'
6
+ require_relative 'schema_plus_pg_indexes/middleware/postgresql/dumper'
7
+ require_relative 'schema_plus_pg_indexes/middleware/postgresql/migration'
8
+ require_relative 'schema_plus_pg_indexes/middleware/postgresql/query'
9
+ require_relative 'schema_plus_pg_indexes/version'
10
+
11
+ SchemaMonkey.register(SchemaPlusPgIndexes)
@@ -0,0 +1,49 @@
1
+ module SchemaPlusPgIndexes
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ #
5
+ # SchemaPlusPgIndexes extends the IndexDefinition object to return information
6
+ # case sensitivity, expessions, and operator classes
7
+ module IndexDefinition
8
+
9
+ def self.included(base)
10
+ base.alias_method_chain :initialize, :schema_plus_pg_indexes
11
+ end
12
+
13
+ attr_accessor :expression
14
+ attr_accessor :operator_classes
15
+
16
+ def case_sensitive?
17
+ @case_sensitive
18
+ end
19
+
20
+ def conditions
21
+ ActiveSupport::Deprecation.warn "ActiveRecord IndexDefinition#conditions is deprecated, used #where instead"
22
+ where
23
+ end
24
+
25
+ def kind
26
+ ActiveSupport::Deprecation.warn "ActiveRecord IndexDefinition#kind is deprecated, used #using instead"
27
+ using
28
+ end
29
+
30
+ def initialize_with_schema_plus_pg_indexes(*args)
31
+ initialize_without_schema_plus_pg_indexes(*args)
32
+ options = args.dup.extract_options!
33
+ @expression = options[:expression]
34
+ @operator_classes = options[:operator_classes] || {}
35
+ @case_sensitive = options.include?(:case_sensitive) ? options[:case_sensitive] : true
36
+ end
37
+
38
+ def ==(other)
39
+ return false if not super(other) # can use super here because == is defined in SchemaPlusIndexes which was included before us
40
+ return false unless self.expression == other.expression
41
+ return false unless !!self.case_sensitive? == !!other.case_sensitive?
42
+ return false unless self.operator_classes == other.operator_classes
43
+ return true
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ module SchemaPlusPgIndexes
2
+ module Middleware
3
+ module Migration
4
+
5
+ def self.insert
6
+ SchemaMonkey::Middleware::Migration::Index.prepend DeprecateArgs
7
+ end
8
+
9
+ class DeprecateArgs < SchemaMonkey::Middleware::Base
10
+ def call(env)
11
+ {:conditions => :where, :kind => :using}.each do |deprecated, proper|
12
+ if env.options[deprecated]
13
+ ActiveSupport::Deprecation.warn "ActiveRecord index option #{deprecated.inspect} is deprecated, use #{proper.inspect} instead"
14
+ env.options[proper] = env.options.delete(deprecated)
15
+ end
16
+ end
17
+ continue env
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ module SchemaPlusPgIndexes
2
+ module Middleware
3
+ module Postgresql
4
+ module Dumper
5
+
6
+ def self.insert
7
+ SchemaMonkey::Middleware::Dumper::Indexes.append DumpExtensions
8
+ SchemaMonkey::Middleware::Dumper::Table.append InlineIndexes
9
+ end
10
+
11
+ class DumpExtensions < SchemaMonkey::Middleware::Base
12
+ def call(env)
13
+ continue env
14
+
15
+ index_defs = Dumper.get_index_defiinitions(env, env.table)
16
+
17
+ env.table.indexes.each do |index_dump|
18
+ index_def = index_defs.find(&its.name == index_dump.name)
19
+ if index_def.columns.blank?
20
+ index_dump.add_option "expression: #{index_def.expression.inspect}" if index_def.expression and index_def.columns.blank?
21
+ else
22
+ index_dump.add_option "case_sensitive: false" unless index_def.case_sensitive?
23
+ unless index_def.operator_classes.blank?
24
+ if index_def.operator_classes.values.uniq.length == 1
25
+ index_dump.add_option "operator_class: #{index_def.operator_classes.values.first.inspect}"
26
+ else
27
+ index_dump.add_option "operator_class: {" + index_def.operator_classes.map{|column, val| "#{column.inspect}=>#{val.inspect}"}.join(", ") + "}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ class InlineIndexes < SchemaMonkey::Middleware::Base
36
+ def call(env)
37
+ continue env
38
+
39
+ index_defs = Dumper.get_index_defiinitions(env, env.table)
40
+
41
+ env.table.indexes.select(&its.columns.blank?).each do |index|
42
+ env.table.statements << "t.index name: #{index.name.inspect}, #{index.options}"
43
+ env.table.indexes.delete(index)
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.get_index_defiinitions(env, table_dump)
49
+ env.dump.data.index_definitions ||= {}
50
+ env.dump.data.index_definitions[table_dump.name] ||= env.connection.indexes(table_dump.name)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,83 @@
1
+ module SchemaPlusPgIndexes
2
+ module Middleware
3
+ module Postgresql
4
+ module Migration
5
+ def self.insert
6
+ SchemaMonkey::Middleware::Migration::IndexComponentsSql.append DefineExtensions
7
+ end
8
+
9
+ class DefineExtensions < SchemaMonkey::Middleware::Base
10
+ # SchemaPlusPgIndexes provides the following extra options for PostgreSQL
11
+ # indexes:
12
+ # * +:expression+ - SQL expression to index. column_name can be nil or ommitted, in which case :name must be provided
13
+ # * +:operator_class+ - an operator class name or a hash mapping column name to operator class name
14
+ # * +:case_sensitive - setting to +false+ is a shorthand for :expression => 'LOWER(column_name)'
15
+ #
16
+ # The <tt>:case_sensitive => false</tt> option ties in with Rails built-in support for case-insensitive searching:
17
+ # validates_uniqueness_of :name, :case_sensitive => false
18
+ #
19
+ # Since since <tt>:case_sensitive => false</tt> is implemented by
20
+ # using <tt>:expression</tt>, this raises an ArgumentError if both
21
+ # are specified simultaneously.
22
+ #
23
+ def call(env)
24
+ options = env.options
25
+ column_names = env.column_names
26
+ table_name = env.table_name
27
+ connection = env.connection
28
+
29
+ if env.column_names.empty?
30
+ raise ArgumentError, "No columns and :expression missing from options - cannot create index" unless options[:expression]
31
+ raise ArgumentError, "No columns, and index name not given. Pass :name option" unless options[:name]
32
+ end
33
+
34
+ expression = options.delete(:expression)
35
+ operator_classes = options.delete(:operator_class)
36
+ case_insensitive = (options.delete(:case_sensitive) == false)
37
+
38
+ if expression
39
+ raise ArgumentError, "Cannot specify :case_sensitive => false with an expression. Use LOWER(column_name)" if case_insensitive
40
+ expression.strip!
41
+ if m = expression.match(/^using\s+(?<using>\S+)\s*(?<rest>.*)/i)
42
+ options[:using] = m[:using]
43
+ expression = m[:rest]
44
+ end
45
+ if m = expression.match(/^(?<rest>.*)\s+where\s+(?<where>.*)/i)
46
+ options[:where] = m[:where]
47
+ expression = m[:rest]
48
+ end
49
+ end
50
+
51
+ continue env
52
+
53
+ if operator_classes and not operator_classes.is_a? Hash
54
+ operator_classes = Hash[column_names.map {|name| [name, operator_classes]}]
55
+ end
56
+
57
+ if expression
58
+ env.sql.columns = expression.sub(/ ^\( (.*) \) $/x, '\1')
59
+ elsif operator_classes or case_insensitive
60
+ option_strings = Hash[column_names.map {|name| [name, '']}]
61
+ (operator_classes||{}).stringify_keys.each do |column, opclass|
62
+ option_strings[column] += " #{opclass}" if opclass
63
+ end
64
+ option_strings = connection.send :add_index_sort_order, option_strings, column_names, options
65
+
66
+ if case_insensitive
67
+ caseable_columns = connection.columns(table_name).select { |col| [:string, :text].include?(col.type) }.map(&:name)
68
+ quoted_column_names = column_names.map do |col_name|
69
+ (caseable_columns.include?(col_name.to_s) ? "LOWER(#{connection.quote_column_name(col_name)})" : connection.quote_column_name(col_name)) + option_strings[col_name]
70
+ end
71
+ else
72
+ quoted_column_names = column_names.map { |col_name| connection.quote_column_name(col_name) + option_strings[col_name] }
73
+ end
74
+
75
+ env.sql.columns = quoted_column_names.join(', ')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,130 @@
1
+ module SchemaPlusPgIndexes
2
+ module Middleware
3
+ module Postgresql
4
+ module Query
5
+ def self.insert
6
+ SchemaMonkey::Middleware::Query::Indexes.append LookupExtensions
7
+ end
8
+
9
+ class LookupExtensions < SchemaMonkey::Middleware::Base
10
+
11
+ def get_opclass_names(env, opclasses)
12
+ @opclass_names ||= {}
13
+ if (missing = opclasses - @opclass_names.keys).any?
14
+ result = env.connection.query(<<-SQL, 'SCHEMA')
15
+ SELECT oid, opcname FROM pg_opclass
16
+ WHERE (NOT opcdefault) AND oid IN (#{opclasses.join(',')})
17
+ SQL
18
+ result.each do |oid, opcname|
19
+ @opclass_names[oid] = opcname
20
+ end
21
+ end
22
+ end
23
+
24
+ def call(env)
25
+ # Ideally we'd let AR do its stuff and then add the extras.
26
+ #
27
+ # But one of the extras is expressions. AR completely strips out
28
+ # indexes with expressions, so to handle them we need to
29
+ # essentially reissue the original query and then duplicate what
30
+ # AR does to process them. That being the case we may as well
31
+ # just skip AR's implementation and use ours.
32
+ #
33
+ # We could limit that query to just those indexes that have
34
+ # expressions, but we'd still have our code duplicating the AR
35
+ # code. Plus, our own query can handle operator classess at the
36
+ # same time, but to add operator_classes to AR's definitions we'd
37
+ # still have to issue additional queries. Plus, using our own
38
+ # query we have the opportunity to handle tables of the form
39
+ # 'namespace.tablename'
40
+ #
41
+ # So, we use our code and DO NOT DO:
42
+ #
43
+ # continue env
44
+ #
45
+ result = env.connection.query(<<-SQL, 'SCHEMA')
46
+
47
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
48
+ m.amname, pg_get_expr(d.indpred, t.oid) as conditions, pg_get_expr(d.indexprs, t.oid) as expression,
49
+ d.indclass
50
+ FROM pg_class t
51
+ INNER JOIN pg_index d ON t.oid = d.indrelid
52
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
53
+ INNER JOIN pg_am m ON i.relam = m.oid
54
+ WHERE i.relkind = 'i'
55
+ AND d.indisprimary = 'f'
56
+ AND t.relname = '#{table_name_without_namespace(env.table_name)}'
57
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(env.table_name)} )
58
+ ORDER BY i.relname
59
+ SQL
60
+
61
+ env.index_definitions += result.map do |(index_name, unique, indkey, inddef, oid, using, conditions, expression, indclass)|
62
+ index_keys = indkey.split(" ")
63
+ opclasses = indclass.split(" ")
64
+
65
+ rows = env.connection.query(<<-SQL, 'SCHEMA')
66
+ SELECT CAST(a.attnum as VARCHAR), a.attname, t.typname
67
+ FROM pg_attribute a
68
+ INNER JOIN pg_type t ON a.atttypid = t.oid
69
+ WHERE a.attrelid = #{oid}
70
+ SQL
71
+ columns = {}
72
+ types = {}
73
+ rows.each do |num, name, type|
74
+ columns[num] = name
75
+ types[name] = type
76
+ end
77
+
78
+ column_names = columns.values_at(*index_keys).compact
79
+ case_sensitive = true
80
+
81
+ # extract column names from the expression, for a
82
+ # case-insensitive index.
83
+ # only applies to character, character varying, and text
84
+ if expression
85
+ rexp_lower = %r{\blower\(\(?([^)]+)(\)::text)?\)}
86
+ if expression.match /\A#{rexp_lower}(?:, #{rexp_lower})*\z/
87
+ case_insensitive_columns = expression.scan(rexp_lower).map(&:first).select{|column| %W[char varchar text].include? types[column]}
88
+ if case_insensitive_columns.any?
89
+ case_sensitive = false
90
+ column_names = index_keys.map { |index_key|
91
+ index_key == '0' ? case_insensitive_columns.shift : columns[index_key]
92
+ }.compact
93
+ end
94
+ end
95
+ end
96
+
97
+ get_opclass_names(env, opclasses)
98
+ operator_classes = Hash[
99
+ index_keys.zip(opclasses).map { |index_key, opclass| [columns[index_key], @opclass_names[opclass]] }
100
+ ]
101
+ operator_classes.delete_if{|k,v| v.nil?}
102
+
103
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
104
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
105
+ orders = desc_order_columns.any? ? Hash[column_names.map {|column| [column, desc_order_columns.include?(column) ? :desc : :asc]}] : {}
106
+
107
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(env.table_name, column_names,
108
+ :name => index_name,
109
+ :unique => (unique == 't'),
110
+ :orders => orders,
111
+ :where => conditions,
112
+ :case_sensitive => case_sensitive,
113
+ :using => using.downcase == "btree" ? nil : using.to_sym,
114
+ :operator_classes => operator_classes,
115
+ :expression => expression)
116
+ end
117
+ end
118
+
119
+ def namespace_sql(table_name)
120
+ (table_name.to_s =~ /(.*)[.]/) ? "'#{$1}'" : "ANY (current_schemas(false))"
121
+ end
122
+
123
+ def table_name_without_namespace(table_name)
124
+ table_name.to_s.sub /.*[.]/, ''
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end