schema_plus_pg_indexes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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