Skip to content

Commit

Permalink
almost works
Browse files Browse the repository at this point in the history
  • Loading branch information
funny-falcon committed Apr 22, 2010
0 parents commit a1b87af
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -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 changes: 36 additions & 0 deletions 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
22 changes: 22 additions & 0 deletions Rakefile
@@ -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
2 changes: 2 additions & 0 deletions init.rb
@@ -0,0 +1,2 @@
# Include hook code here
require 'postgres_arrays'
1 change: 1 addition & 0 deletions install.rb
@@ -0,0 +1 @@
# Install hook code here
283 changes: 283 additions & 0 deletions lib/postgres_arrays.rb
@@ -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

4 changes: 4 additions & 0 deletions tasks/postgres_arrays_tasks.rake
@@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :postgres_arrays do
# # Task goes here
# end
8 changes: 8 additions & 0 deletions test/postgres_arrays_test.rb
@@ -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
1 change: 1 addition & 0 deletions uninstall.rb
@@ -0,0 +1 @@
# Uninstall hook code here

0 comments on commit a1b87af

Please sign in to comment.