Permalink
Browse files

almost works

  • Loading branch information...
0 parents commit a1b87afca8afcfd41d2ff808e5090de2161cf09d @funny-falcon committed Apr 22, 2010
Showing with 377 additions and 0 deletions.
  1. +20 −0 MIT-LICENSE
  2. +36 −0 README
  3. +22 −0 Rakefile
  4. +2 −0 init.rb
  5. +1 −0 install.rb
  6. +283 −0 lib/postgres_arrays.rb
  7. +4 −0 tasks/postgres_arrays_tasks.rake
  8. +8 −0 test/postgres_arrays_test.rb
  9. +1 −0 uninstall.rb
@@ -0,0 +1,20 @@
+Copyright (c) 2010 Sokolov Yura aka funny_falcon
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36 README
@@ -0,0 +1,36 @@
+PostgresArrays
+==============
+
+This library adds ability to use PostgreSQL array types with ActiveRecord.
+
+ > User.find(:all, :conditions=>['arr @> ?', [1,2,3].pg])
+ SELECT * FROM "users" WHERE ('arr' @> E'{"1", "2", "3"}')
+ > User.find(:all, :conditions=>['arr @> ?', [1,2,3].pg(:integer)])
+ SELECT * FROM "users" WHERE (arr @> '{1,2,3}')
+ > User.find(:all, :conditions=>['arr @> ?', [1,2,3].pg(:float)])
+ SELECT * FROM "users" WHERE (arr @> '{1.0,2.0,3.0}')
+ > u = User.find(1)
+ SELECT * FROM "users" WHERE ("users"."id" = 1)
+ => #<User id: 1, ..., arr: [1,2]>
+ > u.arr = [3,4]
+ > u.save
+ UPDATE "users" SET "db_ar" = '{3.0,4.0}' WHERE "id" = 19
+ > User.find(:all, :conditions=>{:arr=>[3,4].pg})
+ SELECT * FROM "users" WHERE ("users"."arr" = E'{"3", "4"}')
+ > User.find(:all, :conditions=>{:arr=>[3,4].search_any(:float)})
+ SELECT * FROM "users" WHERE ("users"."arr" && '{3.0,4.0}')
+ > User.find(:all, :conditions=>{:arr=>[3,4].search_all(:integer)})
+ SELECT * FROM "users" WHERE ("users"."arr" @> '{3,4}')
+ > User.find(:all, :conditions=>{:arr=>[3,4].search_subarray(:safe)})
+ SELECT * FROM "users" WHERE ("users"."arr" <@ '{3,4}')
+
+ class U < ActiveRecord::Migration
+ def self.up
+ add_column :users, :fl_ar, :float_array
+ end
+ end
+
+Plugin developed and used with rails 2.3.5 at the moment.
+
+
+Copyright (c) 2010 Sokolov Yura aka funny_falcon, released under the MIT license
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the postgres_arrays plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the postgres_arrays plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'PostgresArrays'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
@@ -0,0 +1,2 @@
+# Include hook code here
+require 'postgres_arrays'
@@ -0,0 +1 @@
+# Install hook code here
@@ -0,0 +1,283 @@
+# PostgresArrays
+require 'active_record'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLColumn < Column #:nodoc:
+ BASE_TYPE_COLUMNS = Hash.new{|h, base_type|
+ base_column= new(nil, nil, base_type, true)
+ h[base_type] = h[base_column.type]= base_column
+ }
+ attr_reader :base_type_column
+
+ def initialize(name, default, sql_type = nil, null = true)
+ if sql_type =~ /^(.+)\[\]$/
+ @base_sql_type = $1
+ @base_column = BASE_TYPE_COLUMNS[@base_sql_type]
+ end
+ super(name, self.class.extract_value_from_default(default), sql_type, null)
+ end
+
+ def simplified_type_with_postgresql_arrays(field_type)
+ if field_type=~/^(.+)\[\]$/
+ :"#{simplified_type_without_postgresql_arrays($1)}_array"
+ else
+ simplified_type_without_postgresql_arrays(field_type)
+ end
+ end
+ alias_method_chain :simplified_type, :postgresql_arrays
+
+ def klass
+ if type.to_s =~ /_array$/
+ Array
+ else
+ super
+ end
+ end
+
+ def type_cast(value)
+ return nil if value.nil?
+ case type
+ when :integer_array, :float_array then self.class.string_to_num_array(value)
+ when :decimal_array, :date_array, :boolean_array
+ safe_string_to_array(value)
+ when :timestamp_array, :time_array, :datetime_array, :binary_array
+ string_to_array(value)
+ when :text_array, :string_array then self.class.string_to_text_array(value)
+ else super
+ end
+ end
+
+ def type_cast_code(var_name)
+ case type
+ when :integer_array, :float_array
+ "#{self.class.name}.string_to_num_array(#{var_name})"
+ when :decimal_array, :date_array, :boolean_array
+ "#{self.class.name}.safe_string_to_array(#{var_name}, #{@base_sql_type.inspect})"
+ when :timestamp_array, :time_array, :datetime_array, :binary_array
+ "#{self.class.name}.string_to_array(#{var_name}, #{@base_sql_type.inspect})"
+ when :text_array, :string_array
+ "#{self.class.name}.string_to_text_array(#{var_name})"
+ else super
+ end
+ end
+
+ def safe_string_to_array(string)
+ return string unless string.is_a? String
+ return nil if string.empty?
+
+ string[1...-1].split(',').map{|v| @base_column.type_cast(v)}
+ end
+
+ def string_to_array(string)
+ return string unless string.is_a? String
+ return nil if string.empty?
+
+ self.class.string_to_text_array(string).map{|v| @base_column.type_cast(v)}
+ end
+
+ def self.safe_string_to_array(string, sql_type)
+ return string unless string.is_a? String
+ return nil if string.empty?
+
+ base_column = BASE_TYPE_COLUMNS[sql_type]
+ string[1...-1].split(',').map{|v| base_column.type_cast(v)}
+ end
+
+ def self.string_to_array(string, sql_type)
+ return string unless string.is_a? String
+ return nil if string.empty?
+
+ base_column = BASE_TYPE_COLUMNS[sql_type]
+ string_to_text_array( string ).map{|v| base_column.type_cast(v)}
+ end
+
+ def self.string_to_num_array(string)
+ return string unless string.is_a? String
+ return nil if string.empty?
+
+ eval(string.tr('{}','[]'))
+ end
+
+ SARRAY_QUOTED = /^"(.*[^\\])?"$/m
+ SARRAY_PARTIAL = /^".*(\\"|[^"])$/m
+ def self.string_to_text_array(value)
+ return value unless value.is_a? String
+ return nil if value.empty?
+
+ values = value[1...-1].split(',')
+ partial = false
+ values.inject([]) do |res, s|
+ if partial
+ s = res.pop << ",#{s}"
+ elsif s=~ SARRAY_PARTIAL
+ partial = true
+ end
+ if s =~ SARRAY_QUOTED
+ s = eval(s)
+ partial = false
+ elsif s == 'NULL'
+ s = nil
+ end
+ res << s
+ end
+ end
+ end
+
+ class PostgreSQLAdapter < AbstractAdapter
+ def quote_with_postgresql_arrays(value, column = nil)
+ if Array === value && column && "#{column.type}" =~ /^(.+)_array$/
+ quote_array_by_base_type(value, $1)
+ else
+ quote_without_postgresql_arrays(value, column = nil)
+ end
+ end
+ alias_method_chain :quote, :postgresql_arrays
+
+ def quote_array_by_base_type(value, base_type, column = nil)
+ case base_type.to_sym
+ when :integer then quote_pg_integer_array(value)
+ when :float then quote_pg_float_array(value)
+ when :string, :text, :other then quote_pg_text_array(value)
+ when :decimal, :boolean, :date, :safe then quote_pg_string_safe_array(value)
+ else quote_pg_string_array(value, base_type, column)
+ end
+ end
+
+ def quote_pg_integer_array(value)
+ "'{#{ value.map{|v| v.nil? ? 'NULL' : v.to_i}.join(',')}}'"
+ end
+
+ def quote_pg_float_array(value)
+ "'{#{ value.map{|v| v.nil? ? 'NULL' : v.to_f}.join(',')}}'"
+ end
+
+ def quote_pg_string_safe_array(value)
+ "'{#{ value.map{|v| v.nil? ? 'NULL' : v.to_s}.join(',')}}'"
+ end
+
+ def quote_pg_string_array(value, base_type, column=nil)
+ base_type_column= if column
+ column.base_type_column
+ else
+ PostgreSQLColumn::BASE_TYPE_COLUMNS[base_type.to_sym]
+ end
+ value = value.map do|v|
+ v = quote_without_postgresql_arrays(v, base_type_column)
+ if v=~/^E?'(.+)'$/ then v = $1 end
+ "\"#{v.gsub('"','\"')}\""
+ end
+ "'{#{ value.join(',')}}'"
+ end
+
+ def quote_pg_text_array(value)
+ value = value.map{|v|
+ v ? (v = quote_string(v.to_s); v.gsub!('"','\"'); v) : 'NULL'
+ }.inspect
+ value.tr!('[]','{}')
+ "E'#{value}'"
+ end
+
+ NATIVE_DATABASE_TYPES.keys.each do |key|
+ unless key==:primary_key
+ base = NATIVE_DATABASE_TYPES[key].dup
+ base[:name] = base[:name]+'[]'
+ NATIVE_DATABASE_TYPES[:"{key}_array"]= base
+ end
+ end
+
+ def type_to_sql_with_postgresql_arrays(type, limit = nil, precision = nil, scale = nil)
+ if type.to_s =~ /^(.+)_array$/
+ type_to_sql_without_postgresql_arrays($1, limit, precision, scale)+'[]'
+ else
+ type_to_sql_without_postgresql_arrays(type, limit, precision, scale)
+ end
+ end
+
+ alias_method_chain :type_to_sql, :postgresql_arrays
+ end
+ end
+
+ class Base
+ class << self
+ def attribute_condition_with_postgresql_arrays(quoted_column_name, argument)
+ if ::PGArrays::PgArray === argument
+ case argument
+ when ::PGArrays::PgAny then "#{quoted_column_name} && ?"
+ when ::PGArrays::PgAll then "#{quoted_column_name} @> ?"
+ when ::PGArrays::PgIncludes then "#{quoted_column_name} <@ ?"
+ else "#{quoted_column_name} = ?"
+ end
+ else
+ attribute_condition_without_postgresql_arrays(quoted_column_name, argument)
+ end
+ end
+ alias_method_chain :attribute_condition, :postgresql_arrays
+
+ def quote_bound_value_with_postgresql_arrays(value)
+ if ::PGArrays::PgArray === value
+ connection.quote_array_by_base_type(value, value.base_type)
+ else
+ quote_bound_value_without_postgresql_arrays(value)
+ end
+ end
+ alias_method_chain :quote_bound_value, :postgresql_arrays
+ end
+ end
+end
+
+module PGArrays
+ class PgArray < Array
+ attr_reader :base_type
+ def initialize(array, type=nil)
+ super(array)
+ @base_type = type if type
+ end
+ def base_type
+ @base_type || :other
+ end
+ end
+
+ class PgAny < PgArray
+ # this is for cancan
+ if defined? CanCan::Ability
+ def include?(v)
+ Array === v && !( v & self ).empty?
+ end
+ end
+ end
+
+ class PgAll < PgArray
+ if defined? CanCan::Ability
+ def include?
+ Array === v && (self - v).empty?
+ end
+ end
+ end
+
+ class PgIncludes < PgArray
+ if defined? CanCan::Ability
+ def include?
+ Array === v && (v - self).empty?
+ end
+ end
+ end
+end
+
+class Array
+ def pg(type=nil)
+ ::PGArrays::PgArray.new(self, type)
+ end
+ def search_any(type=nil)
+ ::PGArrays::PgAny.new(self, type)
+ end
+ def search_all(type=nil)
+ ::PGArrays::PgAll.new(self, type)
+ end
+ def search_subarray(type=nil)
+ ::PGArrays::PgIncludes.new(self, type)
+ end
+end
+
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :postgres_arrays do
+# # Task goes here
+# end
@@ -0,0 +1,8 @@
+require 'test/unit'
+
+class PostgresArraysTest < Test::Unit::TestCase
+ # Replace this with your real tests.
+ def test_this_plugin
+ flunk
+ end
+end
@@ -0,0 +1 @@
+# Uninstall hook code here

0 comments on commit a1b87af

Please sign in to comment.