activerecord-metal 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.
- data/README.md +126 -0
- data/lib/active_record/metal.rb +67 -0
- data/lib/active_record/metal/logging.rb +43 -0
- data/lib/active_record/metal/postgresql.rb +126 -0
- data/lib/active_record/metal/postgresql/conversions.rb +170 -0
- data/lib/active_record/metal/postgresql/conversions/etest.rb +122 -0
- data/lib/active_record/metal/postgresql/etest.rb +79 -0
- data/lib/active_record/metal/postgresql/import.rb +141 -0
- data/lib/active_record/metal/postgresql/queries.rb +129 -0
- data/lib/active_record/metal/transaction.rb +42 -0
- data/lib/active_record/metal/version.rb +5 -0
- data/lib/activerecord-metal.rb +1 -0
- data/test/pg_test.rb +15 -0
- data/test/test_helper.rb +37 -0
- metadata +77 -0
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# activerecord-metal
|
2
|
+
|
3
|
+
Because sometimes you need just SQL only.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
gem install activerecord-metal
|
8
|
+
|
9
|
+
## Usage - Querying
|
10
|
+
|
11
|
+
The initial impulse to build activerecord-metal is that I needed to run custom SQL
|
12
|
+
queries and wanted properly typed results - something that ActiveRecord gives you
|
13
|
+
only for columns that are defined in the table layout, (and "count", probably),
|
14
|
+
but not for custom calculated values.
|
15
|
+
|
16
|
+
So there is support for queries.
|
17
|
+
|
18
|
+
# use the current ActiveRecord::Base connection
|
19
|
+
metal = ActiveRecord::Metal.new
|
20
|
+
|
21
|
+
# ask for single results
|
22
|
+
metal.ask "SELECT 2+4" # => 6
|
23
|
+
metal.ask "SELECT '2+4=6'" # => '2+4=6'
|
24
|
+
|
25
|
+
# properly deduce types
|
26
|
+
metal.ask "SELECT '1999-01-01 00:00:00'::timestamp" # => returns a Time object
|
27
|
+
|
28
|
+
# iterate over results
|
29
|
+
metal.exec("SELECT id, name FROM users") do |id, name|
|
30
|
+
# do something with id and name
|
31
|
+
# ...
|
32
|
+
end
|
33
|
+
|
34
|
+
# also metal.exec returns all rows in an array, which also
|
35
|
+
# contains some type information.
|
36
|
+
results = metal.exec("SELECT id, name FROM users") # => ary
|
37
|
+
results.types # => [ Numerical, String ]
|
38
|
+
results.columns # => [ "id", "name" ]
|
39
|
+
|
40
|
+
# This information is contained in each of the rows also
|
41
|
+
row = results.first
|
42
|
+
row.types # => [ Numerical, String ]
|
43
|
+
row.columns # => [ "id", "name" ]
|
44
|
+
|
45
|
+
# use positional parameters: note: Rails' '?' placeholders don't work here,
|
46
|
+
# only what is the database's default; and Postgresql uses $1, $2, ...
|
47
|
+
metal.exec("SELECT id, name FROM users WHERE name=$1", "me")
|
48
|
+
|
49
|
+
## Usage - Mass import
|
50
|
+
|
51
|
+
# Mass imports for array records
|
52
|
+
#
|
53
|
+
records = [
|
54
|
+
[1, "first user"],
|
55
|
+
[2, "second user"],
|
56
|
+
[3, "third user"]
|
57
|
+
]
|
58
|
+
|
59
|
+
#
|
60
|
+
metal.import "users", records # fill from left
|
61
|
+
metal.import "users", records, :columns => [ "id", "name" ] # preferred
|
62
|
+
|
63
|
+
# Mass imports for hash records
|
64
|
+
#
|
65
|
+
records = [
|
66
|
+
{id:1, name:"first user", email:"first-user@inter.net"},
|
67
|
+
{id:2, name:"second user"},
|
68
|
+
{id:3, name:"third user"}
|
69
|
+
]
|
70
|
+
|
71
|
+
#
|
72
|
+
metal.import "users", records
|
73
|
+
|
74
|
+
### How fast is the mass import?
|
75
|
+
|
76
|
+
The metal's importer is faster than ActiveRecord::Base's, because:
|
77
|
+
|
78
|
+
- it is using one prepared statement per import session, as (AFAIK, again)
|
79
|
+
one prepared statement per individual record
|
80
|
+
- it does not fetch ids etc.
|
81
|
+
- it does not do all the rails shananiggins: no
|
82
|
+
|
83
|
+
Hmm, lets see: (Note: This is on a Macbook Air w/Ruby 1.9.2 and activerecord 2.3 - YMMV)
|
84
|
+
|
85
|
+
1.9.2 ~/projects/gem/activerecord-metal[master] > bundle exec script/console
|
86
|
+
Loaded /Users/eno/.irbrc
|
87
|
+
irb(main):001:0> ActiveRecord::Metal::Postgresql.etest
|
88
|
+
Loaded suite ActiveRecord::Metal::Postgresql::Etest
|
89
|
+
Started
|
90
|
+
....
|
91
|
+
INFO -- : 7.8 msecs INSERT 100 hashes into alloys
|
92
|
+
INFO -- : 6.8 msecs INSERT 100 arrays into alloys
|
93
|
+
INFO -- : 63.6 msecs Import 100 hashes via ActiveRecord::Base
|
94
|
+
INFO -- : 670.3 msecs INSERT 10000 hashes into alloys
|
95
|
+
INFO -- : 676.9 msecs INSERT 10000 arrays into alloys
|
96
|
+
INFO -- : 6520.9 msecs Import 10000 hashes via ActiveRecord::Base
|
97
|
+
|
98
|
+
A 10 times speed up sounds massive (and will probably become even faster once
|
99
|
+
the `COPY FROM STDIN` importer is completed)), but on the other hand
|
100
|
+
ActiveRecord::Base imports a single record in less than 700 microseconds,
|
101
|
+
which is probably fast enough for most cases.
|
102
|
+
|
103
|
+
## Why not ActiveRecord::Base?
|
104
|
+
|
105
|
+
ActiveRecord::Base is limited in several ways. It does not
|
106
|
+
|
107
|
+
- defer data types from what is in the database - unless it is a column in one of your models.
|
108
|
+
- sometimes builds very slow queries
|
109
|
+
- use prepared statements (AFAIK)
|
110
|
+
- activerecord < 4 does not make use of hstore types
|
111
|
+
|
112
|
+
## Why not ActiveRecord::Metal?
|
113
|
+
|
114
|
+
ActiveRecord::Metal depends heavily on features of specific databases and
|
115
|
+
adapters. It currently works only on Postgres. Your SQL must be adapted
|
116
|
+
to the database server. There is no validation (except what you are
|
117
|
+
implementing in SQL). And of course: the code base is not exactly mature ;)
|
118
|
+
|
119
|
+
And documentation is still missing. Code coverage is good, though:
|
120
|
+
|
121
|
+
All Files (97.22% covered at 88.78 hits/line)
|
122
|
+
6 files in total. 288 relevant lines. 280 lines covered and 8 lines missed
|
123
|
+
|
124
|
+
## License
|
125
|
+
|
126
|
+
The activerecord-metal gem is distributed under the terms of the Modified BSD License, see LICENSE.BSD for details.
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
class ActiveRecord::Metal
|
4
|
+
attr :connection
|
5
|
+
|
6
|
+
def initialize(connection = ActiveRecord::Base.connection)
|
7
|
+
@connection = connection
|
8
|
+
|
9
|
+
extend implementation
|
10
|
+
|
11
|
+
initialize_implementation
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# To be overridden by the implementation
|
17
|
+
def initialize_implementation
|
18
|
+
end
|
19
|
+
|
20
|
+
def implementation
|
21
|
+
case connection
|
22
|
+
when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
23
|
+
require_relative "metal/postgresql"
|
24
|
+
ActiveRecord::Metal::Postgresql
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
require_relative "metal/logging"
|
30
|
+
require_relative "metal/transaction"
|
31
|
+
|
32
|
+
class ActiveRecord::Metal
|
33
|
+
include Transaction
|
34
|
+
|
35
|
+
include Logging
|
36
|
+
extend Logging
|
37
|
+
end
|
38
|
+
|
39
|
+
module ActiveRecord::Metal::EtestBase
|
40
|
+
SELF = self
|
41
|
+
|
42
|
+
def self.load_expectation_assertions
|
43
|
+
require "expectation/assertions"
|
44
|
+
extend Expectation::Assertions
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup
|
48
|
+
SELF.load_expectation_assertions
|
49
|
+
end
|
50
|
+
|
51
|
+
def metal
|
52
|
+
@metal ||= ActiveRecord::Metal.new
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module ActiveRecord::Metal::Etest
|
57
|
+
include ActiveRecord::Metal::EtestBase
|
58
|
+
|
59
|
+
def metal
|
60
|
+
@metal ||= ActiveRecord::Metal.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_pg_connection
|
64
|
+
expect! metal.connection => ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
65
|
+
expect! metal.is_a?(ActiveRecord::Metal::Postgresql)
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveRecord::Metal::Logging
|
2
|
+
SELF = self
|
3
|
+
|
4
|
+
@@logger = nil
|
5
|
+
|
6
|
+
def self.logger
|
7
|
+
@@logger ||= ActiveRecord::Base.logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.logger=(logger)
|
11
|
+
@@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def log_benchmark(severity, runtime, msg)
|
15
|
+
@benchmark_depth ||= 0
|
16
|
+
return if @benchmark_depth > 0
|
17
|
+
return unless logger = SELF.logger
|
18
|
+
|
19
|
+
threshold = ActiveRecord::Base.auto_explain_threshold_in_seconds
|
20
|
+
if threshold
|
21
|
+
return if runtime < threshold
|
22
|
+
severity = :info if severity == :debug
|
23
|
+
end
|
24
|
+
|
25
|
+
runtime = "%.1f msecs" % (runtime * 1000)
|
26
|
+
|
27
|
+
unless msg.gsub!(/\{\{runtime\}\}/, runtime)
|
28
|
+
msg = "#{runtime} #{msg}"
|
29
|
+
end
|
30
|
+
|
31
|
+
logger.send severity, msg
|
32
|
+
end
|
33
|
+
|
34
|
+
def benchmark(msg, severity = :info)
|
35
|
+
@benchmark_depth ||= 0
|
36
|
+
@benchmark_depth += 1
|
37
|
+
started_at = Time.now
|
38
|
+
yield
|
39
|
+
ensure
|
40
|
+
@benchmark_depth -= 1
|
41
|
+
log_benchmark severity, Time.now - started_at, msg
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
|
3
|
+
module ActiveRecord::Metal::Postgresql
|
4
|
+
private
|
5
|
+
|
6
|
+
def prepared_statements_by_name
|
7
|
+
@prepared_statements_by_name ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def prepared_statements
|
11
|
+
@prepared_statements ||= Hash.new { |hsh, sql| hsh[sql] = _prepare(sql) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve_query(query)
|
15
|
+
query.is_a?(Symbol) ? prepared_statements_by_name[query] : query
|
16
|
+
end
|
17
|
+
|
18
|
+
def _prepare(sql)
|
19
|
+
name = prepared_statement_name(sql)
|
20
|
+
pg_conn.prepare(name, sql)
|
21
|
+
name = name.to_sym
|
22
|
+
prepared_statements_by_name[name] = sql
|
23
|
+
name
|
24
|
+
end
|
25
|
+
|
26
|
+
public
|
27
|
+
|
28
|
+
def prepare(sql)
|
29
|
+
prepared_statements[sql]
|
30
|
+
end
|
31
|
+
|
32
|
+
def unprepare(query)
|
33
|
+
expect! query => [ Symbol, String ]
|
34
|
+
case query
|
35
|
+
when Symbol
|
36
|
+
exec_("DEALLOCATE PREPARE #{query}")
|
37
|
+
sql = prepared_statements_by_name.delete query
|
38
|
+
prepared_statements.delete sql
|
39
|
+
when String
|
40
|
+
name = prepared_statement_name(query)
|
41
|
+
exec_("DEALLOCATE PREPARE #{name}")
|
42
|
+
prepared_statements.delete query
|
43
|
+
prepared_statements_by_name.delete name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def unprepare_all
|
48
|
+
exec_ "DEALLOCATE PREPARE ALL"
|
49
|
+
@prepared_statements_by_name = @prepared_statements = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# -- raw queries ----------------------------------------------------
|
55
|
+
|
56
|
+
def exec_(sql)
|
57
|
+
# STDERR.puts "exec_ --> #{sql}"
|
58
|
+
result = pg_conn.exec sql
|
59
|
+
result.check
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
def exec_prepared(sym, *args)
|
64
|
+
# STDERR.puts "exec_prepared: #{sym.inspect}"
|
65
|
+
args = args.map do |arg|
|
66
|
+
if arg.is_a?(Hash)
|
67
|
+
ActiveRecord::Metal::Postgresql::Conversions::HStore.escape(arg)
|
68
|
+
else
|
69
|
+
arg
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
result = pg_conn.exec_prepared(sym.to_s, args)
|
74
|
+
result.check
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepared_statement_name(sql)
|
79
|
+
"pg_metal_#{Digest::MD5.hexdigest(sql)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
# -- initialisation -------------------------------------------------
|
83
|
+
|
84
|
+
attr :pg_types, :pg_conn
|
85
|
+
|
86
|
+
def initialize_implementation
|
87
|
+
@pg_conn = connection.instance_variable_get("@connection")
|
88
|
+
@pg_types = load_pg_types
|
89
|
+
|
90
|
+
unprepare_all
|
91
|
+
|
92
|
+
name, installed_version = exec("SELECT name, installed_version FROM pg_available_extensions WHERE name='hstore'").first
|
93
|
+
exec_ "CREATE EXTENSION IF NOT EXISTS hstore" unless installed_version
|
94
|
+
end
|
95
|
+
|
96
|
+
def load_pg_types
|
97
|
+
Hash.new("_default").tap do |hsh|
|
98
|
+
hsh[17] = "bytea"
|
99
|
+
|
100
|
+
connection.select_all("SELECT typelem, typname FROM pg_type").
|
101
|
+
each do |record|
|
102
|
+
typelem, typname = record.values_at "typelem", "typname"
|
103
|
+
hsh[typelem.to_i] = typname
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
public
|
109
|
+
|
110
|
+
def has_table?(name)
|
111
|
+
ask "SELECT 't'::BOOLEAN FROM pg_tables WHERE tablename=$1", name
|
112
|
+
end
|
113
|
+
|
114
|
+
def has_index?(name)
|
115
|
+
ask "SELECT 't'::BOOLEAN FROM pg_indexes WHERE indexname=$1", name
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
require_relative "postgresql/conversions"
|
120
|
+
require_relative "postgresql/queries"
|
121
|
+
require_relative "postgresql/import"
|
122
|
+
|
123
|
+
module ActiveRecord::Metal::Postgresql
|
124
|
+
include Queries
|
125
|
+
include Import
|
126
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module ActiveRecord::Metal::Postgresql::Conversions
|
2
|
+
module Numeric; end
|
3
|
+
module Time; end
|
4
|
+
module Date; end
|
5
|
+
module Boolean; end
|
6
|
+
module String; end
|
7
|
+
module HStore; end
|
8
|
+
|
9
|
+
include Numeric, Time, Date, Boolean, String, HStore
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def resolve_type(symbol)
|
13
|
+
sub_converter = included_modules.detect { |mod| mod.method_defined?(symbol) }
|
14
|
+
sub_converter ? sub_converter::T : String
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ActiveRecord::Metal::Postgresql::Conversions::Numeric
|
19
|
+
T = ::Numeric
|
20
|
+
|
21
|
+
def _numeric(s)
|
22
|
+
s =~ /\D/ ? Float(s) : Integer(s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def _int(s)
|
26
|
+
Integer(s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def _float(s);
|
30
|
+
Float(s)
|
31
|
+
rescue ArgumentError
|
32
|
+
case s
|
33
|
+
when /^Infinity$/ then Float::INFINITY
|
34
|
+
when /^-Infinity$/ then -Float::INFINITY
|
35
|
+
when /^NaN$/ then Float::NAN
|
36
|
+
else raise
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def _money(s)
|
41
|
+
Float s.gsub(/[^-0-9.]/, "")
|
42
|
+
end
|
43
|
+
|
44
|
+
def _oid(s)
|
45
|
+
Integer(s)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module ActiveRecord::Metal::Postgresql::Conversions::Date
|
50
|
+
T = ::Date
|
51
|
+
|
52
|
+
def _date(s)
|
53
|
+
T.parse(s)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module ActiveRecord::Metal::Postgresql::Conversions::Time
|
58
|
+
T = ::Time
|
59
|
+
|
60
|
+
def _time(s)
|
61
|
+
T.parse(s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def _timestamp(s)
|
65
|
+
T.parse(s)
|
66
|
+
end
|
67
|
+
|
68
|
+
def _timetz(s)
|
69
|
+
T.parse(s)
|
70
|
+
end
|
71
|
+
|
72
|
+
def _timestamptz(s)
|
73
|
+
T.parse(s)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module ActiveRecord::Metal::Postgresql::Conversions::Boolean
|
78
|
+
T = ::TrueClass
|
79
|
+
|
80
|
+
def _bool(s); s == "t"; end
|
81
|
+
end
|
82
|
+
|
83
|
+
module ActiveRecord::Metal::Postgresql::Conversions::String
|
84
|
+
T = ::String
|
85
|
+
|
86
|
+
def _varchar(s)
|
87
|
+
s
|
88
|
+
end
|
89
|
+
|
90
|
+
def _text(s)
|
91
|
+
s
|
92
|
+
end
|
93
|
+
|
94
|
+
def _default(s)
|
95
|
+
s
|
96
|
+
end
|
97
|
+
|
98
|
+
def _name(s)
|
99
|
+
s
|
100
|
+
end
|
101
|
+
|
102
|
+
def _enum(s)
|
103
|
+
s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
module ActiveRecord::Metal::Postgresql::Conversions::HStore
|
108
|
+
SELF = self
|
109
|
+
|
110
|
+
T = ::Hash
|
111
|
+
NULL = nil
|
112
|
+
|
113
|
+
def _hstore(s)
|
114
|
+
SELF.unescape(s)
|
115
|
+
end
|
116
|
+
|
117
|
+
HSTORE_ESCAPED = /[,\s=>\\]/
|
118
|
+
|
119
|
+
# From activerecord-postgres-hstore, r0.6
|
120
|
+
# Escapes values such that they will work in an hstore string
|
121
|
+
def self.escape_string(str)
|
122
|
+
if str.nil?
|
123
|
+
return 'NULL'
|
124
|
+
end
|
125
|
+
|
126
|
+
str = str.to_s
|
127
|
+
# backslash is an escape character for strings, and an escape character for gsub, so you need 6 backslashes to get 2 in the output.
|
128
|
+
# see http://stackoverflow.com/questions/1542214/weird-backslash-substitution-in-ruby for the gory details
|
129
|
+
str = str.gsub(/\\/, '\\\\\\')
|
130
|
+
# escape backslashes before injecting more backslashes
|
131
|
+
str = str.gsub(/"/, '\"')
|
132
|
+
|
133
|
+
if str =~ HSTORE_ESCAPED or str.empty?
|
134
|
+
str = '"%s"' % str
|
135
|
+
end
|
136
|
+
|
137
|
+
return str
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.escape(hsh, connection=ActiveRecord::Base.connection)
|
141
|
+
hsh.map do |idx, val|
|
142
|
+
"%s=>%s" % [escape_string(idx), escape_string(val)]
|
143
|
+
end * ","
|
144
|
+
end
|
145
|
+
|
146
|
+
# Creates a hash from a valid double quoted hstore format, 'cause this is the format
|
147
|
+
# that postgresql spits out.
|
148
|
+
def self.unescape(str)
|
149
|
+
token_pairs = (str.scan(hstore_pair)).map { |k,v| [k,v =~ /^NULL$/i ? nil : v] }
|
150
|
+
token_pairs = token_pairs.map { |k,v|
|
151
|
+
[ unescape_string(k).to_sym, unescape_string(v) ]
|
152
|
+
}
|
153
|
+
::Hash[ token_pairs ]
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.unescape_string(str)
|
157
|
+
case str
|
158
|
+
when nil then str
|
159
|
+
when /\A"(.*)"\Z/m then $1.gsub(/\\(.)/, '\1')
|
160
|
+
else str.gsub(/\\(.)/, '\1')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.hstore_pair
|
165
|
+
quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
|
166
|
+
unquoted_string = /[^\s=,][^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
|
167
|
+
string = /(#{quoted_string}|#{unquoted_string})/
|
168
|
+
/#{string}\s*=>\s*#{string}/
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#require "active_record/metal/postgresql/conversions/etest"
|
3
|
+
|
4
|
+
module ActiveRecord::Metal::Postgresql::Conversions::Etest
|
5
|
+
include ActiveRecord::Metal::EtestBase
|
6
|
+
|
7
|
+
def test_numeric_types
|
8
|
+
expect! metal.ask("SELECT 1") => 1
|
9
|
+
|
10
|
+
[ "smallint", "integer", "bigint", "decimal", "numeric", "real", "double precision" ].each do |type|
|
11
|
+
expect! metal.ask("SELECT 1::#{type}") => 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_special_numbers
|
16
|
+
expect! metal.ask("SELECT 'Infinity'::real") => Float::INFINITY
|
17
|
+
expect! metal.ask("SELECT '-Infinity'::real") => -Float::INFINITY
|
18
|
+
|
19
|
+
# Can't test for NAN == NAN
|
20
|
+
# expect! metal.ask("SELECT 'NaN'::real") => Float::NAN
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_money
|
24
|
+
expect! metal.ask("SELECT 1234::text::money") => 1234.0
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_strings
|
28
|
+
expect! metal.ask("SELECT 'ok'")
|
29
|
+
expect! metal.ask("SELECT 'good '") => 'good '
|
30
|
+
expect! metal.ask("SELECT 'too long'::varchar(5)") => 'too l'
|
31
|
+
expect! metal.ask("SELECT 'ä'") => 'ä'
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_bytea
|
35
|
+
expect! metal.ask("SELECT '1'::bytea") => '1'
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_dates
|
39
|
+
expect! metal.ask("SELECT '1/18/1999'::date") => Date.parse("18. 1. 1999")
|
40
|
+
expect! metal.ask("SELECT '1999-01-08 04:05:06'::timestamp") => Time.parse("1999-01-08 04:05:06")
|
41
|
+
expect! metal.ask("SELECT TIMESTAMP '1999-01-08 04:05:06'") => Time.parse("1999-01-08 04:05:06")
|
42
|
+
expect! metal.ask("SELECT CURRENT_TIME") => Time
|
43
|
+
expect! metal.ask("SELECT CURRENT_TIMESTAMP") => Time
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_true_and_friends
|
47
|
+
expect! metal.ask("SELECT TRUE") => true
|
48
|
+
expect! metal.ask("SELECT FALSE") => false
|
49
|
+
expect! metal.ask("SELECT NULL") => nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_empty_result
|
53
|
+
expect! metal.ask("SELECT NULL WHERE FALSE") => nil
|
54
|
+
expect! metal.ask("SELECT 1 WHERE FALSE") => nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_column_names
|
58
|
+
result = metal.exec("SELECT 1 as one, 2 as two WHERE FALSE")
|
59
|
+
expect! result.columns == %w(one two)
|
60
|
+
expect! result.empty?
|
61
|
+
|
62
|
+
result = metal.exec("SELECT 1 as one, 2 as two")
|
63
|
+
expect! result.columns == %w(one two)
|
64
|
+
expect! result == [[ 1, 2 ]]
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_hstore
|
68
|
+
result = metal.ask("SELECT 'foo=>foo,bar=>NULL'::hstore")
|
69
|
+
assert_equal result, foo: "foo", bar: nil
|
70
|
+
|
71
|
+
# C = PgTypedQueries::Conversions
|
72
|
+
#
|
73
|
+
# assert_equal C::HStore.escape(a: 1), "'a=>1'::hstore"
|
74
|
+
# assert_equal C::HStore.escape(foo: "foo", bar: nil), "'foo=>foo,bar=>NULL'::hstore"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module ActiveRecord::Metal::Postgresql::Conversions::Etest
|
79
|
+
include ActiveRecord::Metal::EtestBase
|
80
|
+
|
81
|
+
def test_numeric_types_args
|
82
|
+
[ "smallint", "integer", "bigint", "decimal", "numeric", "real", "double precision" ].each do |type|
|
83
|
+
expect! metal.ask("SELECT 1::#{type} WHERE 1=$1", 1) => 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_money_args
|
88
|
+
expect! metal.ask("SELECT 1234::text::money WHERE 1234::text::money=$1", 1234.0) => 1234.0
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_strings_args
|
92
|
+
expect! metal.ask("SELECT 'ok' WHERE 'ok'=$1", 'ok') => 'ok'
|
93
|
+
expect! metal.ask("SELECT 'ä' WHERE 'ä'=$1", 'ä') => 'ä'
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_bytea_args
|
97
|
+
expect! metal.ask("SELECT '1'::bytea WHERE '1'::bytea=$1", '1') => '1'
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_dates_args
|
101
|
+
date = Date.parse("18. 1. 1999")
|
102
|
+
expect! metal.ask("SELECT '1/18/1999'::date WHERE '1/18/1999'::date=$1", date) => date
|
103
|
+
|
104
|
+
ts = Time.parse("1999-01-08 04:05:06")
|
105
|
+
expect! metal.ask("SELECT '1999-01-08 04:05:06'::timestamp WHERE '1999-01-08 04:05:06'::timestamp=$1", ts) => ts
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_true_and_friends_args
|
109
|
+
expect! metal.ask("SELECT TRUE WHERE TRUE=$1", true) => true
|
110
|
+
expect! metal.ask("SELECT FALSE WHERE FALSE=$1", false) => false
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_hstore_args
|
114
|
+
id = metal.ask "INSERT INTO alloys(hsh) VALUES($1) RETURNING id", foo: "foo", bar: nil
|
115
|
+
assert_equal metal.ask("SELECT hsh FROM alloys WHERE id=$1", id), bar: nil, foo: "foo"
|
116
|
+
|
117
|
+
|
118
|
+
result = metal.ask("SELECT 'foo=>foo,bar=>NULL'::hstore WHERE 'foo=>foo,bar=>NULL'::hstore=$1",
|
119
|
+
foo: "foo", bar: nil)
|
120
|
+
assert_equal result, foo: "foo", bar: nil
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ActiveRecord::Metal::Postgresql::Etest
|
4
|
+
include ActiveRecord::Metal::EtestBase
|
5
|
+
|
6
|
+
def test_simple_query
|
7
|
+
expect! metal.ask("SELECT 1") => 1
|
8
|
+
result = metal.exec("SELECT 1 AS number")
|
9
|
+
assert_equal(result, [[1]])
|
10
|
+
assert_equal(result.types, [Numeric])
|
11
|
+
assert_equal(result.columns, ["number"])
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_null_query
|
15
|
+
expect! metal.ask("SELECT 1 AS number WHERE FALSE") => nil
|
16
|
+
result = metal.exec("SELECT 1 AS number WHERE FALSE")
|
17
|
+
assert_equal(result, [])
|
18
|
+
assert_equal(result.types, [Numeric])
|
19
|
+
assert_equal(result.columns, ["number"])
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_positioned_parameters
|
23
|
+
expect! metal.ask("SELECT 1 AS number WHERE 1=$1", 1) => 1
|
24
|
+
assert_equal metal.exec("SELECT 1 AS number WHERE 1=$1", 1), [[1]]
|
25
|
+
assert_equal metal.exec("SELECT 1 AS value WHERE 1=$1", 1), [[1]]
|
26
|
+
assert_equal metal.exec("SELECT 1 AS value WHERE 1=$1", "1"), [[1]]
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_exceptions
|
30
|
+
assert_raise() {
|
31
|
+
metal.exec("SELECT 1 FROM unknown")
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def import_performance(count)
|
36
|
+
return if ENV["ARM_ENV"] == "test" && count > 10
|
37
|
+
|
38
|
+
# -- setup records ------------------------------------------------
|
39
|
+
|
40
|
+
records = 1.upto(count).map do |rec|
|
41
|
+
{
|
42
|
+
num: rand(100000),
|
43
|
+
num2: rand(100000),
|
44
|
+
str1: "alloy_#{Digest::MD5.hexdigest(rand(100000).to_s)}"
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
id = 0
|
49
|
+
values = records.map { |rec|
|
50
|
+
[ (id += 1) ] + rec.values_at(:num, :num2, :str1)
|
51
|
+
}
|
52
|
+
|
53
|
+
# -- run tests ----------------------------------------------------
|
54
|
+
|
55
|
+
metal.ask "DELETE FROM alloys"
|
56
|
+
|
57
|
+
metal.import "alloys", records
|
58
|
+
metal.ask "DELETE FROM alloys"
|
59
|
+
|
60
|
+
metal.import "alloys", values
|
61
|
+
metal.ask "DELETE FROM alloys"
|
62
|
+
|
63
|
+
ActiveRecord::Metal.benchmark "Import #{count} hashes via ActiveRecord::Base" do
|
64
|
+
Alloy.transaction do
|
65
|
+
records.each do |record|
|
66
|
+
Alloy.create! record
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
metal.ask "DELETE FROM alloys"
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_import_performance
|
75
|
+
import_performance(1)
|
76
|
+
import_performance(100)
|
77
|
+
import_performance(10000)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module ActiveRecord::Metal::Postgresql::Import
|
2
|
+
#
|
3
|
+
# Import a number of records into a table.
|
4
|
+
# records is either an Array of Hashes or an Array of Arrays.
|
5
|
+
# In the latter case each record *must* match the order of columns
|
6
|
+
# in the table.
|
7
|
+
def import(table_name, records, options = {})
|
8
|
+
return if records.empty?
|
9
|
+
importer = records.first.is_a?(Hash) ? :hashes : :arrays
|
10
|
+
|
11
|
+
benchmark "INSERT #{records.length} #{importer} into #{table_name}" do
|
12
|
+
expect! table_name => /^\S+$/
|
13
|
+
expect! records.first => [ nil, Hash, Array ]
|
14
|
+
|
15
|
+
case records.length
|
16
|
+
when 0 then :nop
|
17
|
+
when 1 then send("import_#{importer}", table_name, records, options)
|
18
|
+
else transaction { send("import_#{importer}", table_name, records, options) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def import_hashes(table_name, records, _)
|
26
|
+
keys = records.inject([]) do |ary, record|
|
27
|
+
ary | record.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
keys.each { |key| expect! key => Symbol }
|
31
|
+
keys.each { |key| expect! key.to_s => /^\S+$/ }
|
32
|
+
|
33
|
+
values = 1.upto(keys.length).map { |idx| "$#{idx}" }
|
34
|
+
|
35
|
+
sql = "INSERT INTO #{table_name}(#{keys.join(",")}) VALUES(#{values.join(",")})"
|
36
|
+
stmt = prepare(sql)
|
37
|
+
records.each do |record|
|
38
|
+
exec_prepared stmt, *record.values_at(*keys)
|
39
|
+
end
|
40
|
+
rescue
|
41
|
+
logger.warn "#{$!.class.name}: #{$!}"
|
42
|
+
raise
|
43
|
+
ensure
|
44
|
+
unprepare(stmt)
|
45
|
+
end
|
46
|
+
|
47
|
+
def import_arrays(table_name, records, options)
|
48
|
+
columns = if options[:columns]
|
49
|
+
"(" + options[:columns].join(",") + ")"
|
50
|
+
end
|
51
|
+
|
52
|
+
values = 1.upto(records.first.length).map { |idx| "$#{idx}" }
|
53
|
+
|
54
|
+
sql = "INSERT INTO #{table_name}#{columns} VALUES(#{values.join(",")})"
|
55
|
+
stmt = prepare(sql)
|
56
|
+
|
57
|
+
records.each do |record|
|
58
|
+
exec_prepared stmt, *record
|
59
|
+
end
|
60
|
+
rescue
|
61
|
+
logger.warn "#{$!.class.name}: #{$!}"
|
62
|
+
raise
|
63
|
+
ensure
|
64
|
+
unprepare(stmt)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module ActiveRecord::Metal::Postgresql::Import::Etest
|
69
|
+
include ActiveRecord::Metal::EtestBase
|
70
|
+
|
71
|
+
# metal.ask "CREATE TABLE test(num INTEGER, num2 INTEGER, str1 VARCHAR)"
|
72
|
+
|
73
|
+
def test_import_none
|
74
|
+
metal.ask "DELETE FROM alloys"
|
75
|
+
metal.import "alloys", []
|
76
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 0
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_import_array
|
80
|
+
metal.ask "DELETE FROM alloys"
|
81
|
+
metal.import "alloys", [[1,2,"one"]], :columns => %w(num num2 str1)
|
82
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 1
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_import_array_wo_columns
|
86
|
+
metal.ask "DELETE FROM alloys"
|
87
|
+
metal.import "alloys", [[1,1,2,"one"]]
|
88
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 1
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_import_hashes
|
92
|
+
metal.ask "DELETE FROM alloys"
|
93
|
+
metal.import "alloys", [ num: 1, num2: 1, str1: "one" ]
|
94
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
__END__
|
99
|
+
|
100
|
+
# This is example code to load a table via COPY FROM
|
101
|
+
def flush_load(records)
|
102
|
+
copy_data = records.map do |name, value, timestamp, payload|
|
103
|
+
escaped_payload = PgTypedQueries::Conversions::HStore.escape_without_type(payload, connection) if payload
|
104
|
+
escaped_payload ||= "NULL"
|
105
|
+
|
106
|
+
"#{name}|#{value}|#{timestamp.to_i}|#{escaped_payload}\n"
|
107
|
+
end.join
|
108
|
+
|
109
|
+
# STDERR.puts "Running COPY command with #{copy_data.bytesize} bytes for #{records.length} records"
|
110
|
+
|
111
|
+
copy_data = StringIO.new(copy_data)
|
112
|
+
|
113
|
+
connection.transaction do
|
114
|
+
conn = connection.instance_variable_get "@connection"
|
115
|
+
buf = ''
|
116
|
+
conn.exec("COPY #{table_name} FROM STDIN WITH DELIMITER '|' NULL 'NULL'")
|
117
|
+
begin
|
118
|
+
while copy_data.read(256, buf)
|
119
|
+
### Uncomment this to test error-handling for exceptions from the reader side:
|
120
|
+
# raise Errno::ECONNRESET, "socket closed while reading"
|
121
|
+
# $stderr.puts " sending %d bytes of data..." % [ buf.length ]
|
122
|
+
# $stderr.puts "copy #{buf}"
|
123
|
+
until conn.put_copy_data( buf )
|
124
|
+
$stderr.puts " waiting for connection to be writable..."
|
125
|
+
sleep 0.1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# puts "done copying"
|
130
|
+
rescue Errno => err
|
131
|
+
errmsg = "%s while reading copy data: %s" % [ err.class.name, err.message ]
|
132
|
+
conn.put_copy_end(errmsg)
|
133
|
+
else
|
134
|
+
conn.put_copy_end
|
135
|
+
while res = conn.get_result
|
136
|
+
$stderr.puts "Result of COPY is: %s" % [ res.res_status(res.result_status) ]
|
137
|
+
$stderr.puts res.error_message()
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module ActiveRecord::Metal::Postgresql::Queries
|
2
|
+
class ArrayWithTypeInfo < Array
|
3
|
+
attr :columns, :types
|
4
|
+
|
5
|
+
def initialize(values, columns, types)
|
6
|
+
@columns, @types = columns, types
|
7
|
+
replace(values)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Result < Array
|
12
|
+
attr :columns, :types
|
13
|
+
|
14
|
+
def initialize(metal, pg_result, *args)
|
15
|
+
@metal, @pg_result = metal, pg_result
|
16
|
+
@current_row = 0
|
17
|
+
|
18
|
+
setup_colums
|
19
|
+
setup_types
|
20
|
+
end
|
21
|
+
|
22
|
+
def next_row
|
23
|
+
return nil if @current_row >= @pg_result.ntuples
|
24
|
+
|
25
|
+
values = 0.upto(@pg_result.nfields-1).map do |column_number|
|
26
|
+
data = @pg_result.getvalue(@current_row, column_number)
|
27
|
+
data = @converters[column_number].call(data) if data
|
28
|
+
data
|
29
|
+
end
|
30
|
+
|
31
|
+
ArrayWithTypeInfo.new values, columns, types
|
32
|
+
ensure
|
33
|
+
@current_row += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def setup_colums
|
39
|
+
@columns = (0 ... @pg_result.nfields).map { |i| @pg_result.fname(i) }
|
40
|
+
end
|
41
|
+
|
42
|
+
Conversions = ActiveRecord::Metal::Postgresql::Conversions
|
43
|
+
|
44
|
+
def setup_types
|
45
|
+
metal_pg_types = @metal.send(:pg_types)
|
46
|
+
|
47
|
+
pg_types = (0 ... @pg_result.nfields).map do |i|
|
48
|
+
pg_type_id = @pg_result.ftype(i)
|
49
|
+
pg_type = metal_pg_types[pg_type_id] || raise("Unknown pg_type_id: #{pg_type_id}")
|
50
|
+
pg_type.gsub(/\d+$/, "").to_sym
|
51
|
+
end
|
52
|
+
|
53
|
+
# -- ruby types -------------------------------------------------
|
54
|
+
|
55
|
+
@types = pg_types.map do |pg_type|
|
56
|
+
Conversions.resolve_type(pg_type)
|
57
|
+
end
|
58
|
+
|
59
|
+
# -- converters -------------------------------------------------
|
60
|
+
|
61
|
+
pg_conn = @metal.send(:pg_conn)
|
62
|
+
unescape_bytea = lambda { |s| pg_conn.unescape_bytea(s) }
|
63
|
+
|
64
|
+
@converters = pg_types.map do |pg_type|
|
65
|
+
if pg_type == :_bytea
|
66
|
+
unescape_bytea
|
67
|
+
else
|
68
|
+
Conversions.method(pg_type)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def exec(sql, *args, &block)
|
75
|
+
started_at = Time.now
|
76
|
+
|
77
|
+
# prepared queries - denoted by symbols - are executed as such, and
|
78
|
+
# not cleaned up. A caller can get a prepared query by calling
|
79
|
+
# metal.prepare.
|
80
|
+
if sql.is_a?(Symbol)
|
81
|
+
pg_result = exec_prepared(sql, *args)
|
82
|
+
elsif args.empty?
|
83
|
+
pg_result = exec_(sql)
|
84
|
+
else
|
85
|
+
name = prepare(sql)
|
86
|
+
pg_result = exec_prepared(name, *args)
|
87
|
+
unprepare(sql)
|
88
|
+
end
|
89
|
+
|
90
|
+
result = Result.new(self, pg_result, *args)
|
91
|
+
|
92
|
+
rows = []
|
93
|
+
while row = result.next_row
|
94
|
+
yield row if block
|
95
|
+
rows << row
|
96
|
+
end
|
97
|
+
|
98
|
+
ArrayWithTypeInfo.new rows, result.columns, result.types
|
99
|
+
ensure
|
100
|
+
log_benchmark :debug, Time.now - started_at,
|
101
|
+
"SQL: {{runtime}} %s %s" % [ sql.is_a?(Symbol) ? "[P]" : " ", resolve_query(sql) ]
|
102
|
+
end
|
103
|
+
|
104
|
+
def ask(sql, *args)
|
105
|
+
first_row = nil
|
106
|
+
|
107
|
+
catch(:received_first_row) do
|
108
|
+
exec(sql, *args) do |row|
|
109
|
+
first_row = row
|
110
|
+
throw :received_first_row
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
first_row && first_row.first
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module ActiveRecord::Metal::Postgresql::Queries::Etest
|
119
|
+
include ActiveRecord::Metal::EtestBase
|
120
|
+
|
121
|
+
def test_benchmark
|
122
|
+
metal.ask("SELECT 1")
|
123
|
+
metal.ask("SELECT 1 WHERE 1=$1", 1)
|
124
|
+
query = metal.prepare("SELECT 1 WHERE 1=$1")
|
125
|
+
metal.ask(query, 1)
|
126
|
+
metal.unprepare(query)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveRecord::Metal::Transaction
|
2
|
+
# We use ActiveRecord::Base's transaction, because this one supports
|
3
|
+
# nested transactions by automatically falling back to savepoints
|
4
|
+
# when needed.
|
5
|
+
def transaction(mode = :readwrite, &block)
|
6
|
+
if mode == :readonly
|
7
|
+
ro_transaction(&block)
|
8
|
+
else
|
9
|
+
connection.transaction(&block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def ro_transaction(&block)
|
16
|
+
r = nil
|
17
|
+
connection.transaction do
|
18
|
+
r = yield
|
19
|
+
raise ActiveRecord::Rollback
|
20
|
+
end
|
21
|
+
r
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ActiveRecord::Metal::Transaction::Etest
|
26
|
+
def test_transaction_return_values
|
27
|
+
expect! metal.transaction { 1 } => 1
|
28
|
+
expect! metal.transaction(:readonly) { 1 } => 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_transaction
|
32
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 0
|
33
|
+
metal.transaction { metal.ask "INSERT INTO alloys(num) VALUES(1)" }
|
34
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_ro_transaction
|
38
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 0
|
39
|
+
metal.transaction(:readonly) { metal.ask "INSERT INTO alloys(num) VALUES(1)" }
|
40
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys") => 0
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "active_record/metal"
|
data/test/pg_test.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative "test_helper"
|
3
|
+
|
4
|
+
Dir.glob(File.dirname(__FILE__) + "/../lib/active_record/metal/**/etest.rb").each do |file|
|
5
|
+
load file
|
6
|
+
end
|
7
|
+
|
8
|
+
class PostgresTest < Test::Unit::TestCase
|
9
|
+
include ActiveRecord::Metal::Etest
|
10
|
+
include ActiveRecord::Metal::Transaction::Etest
|
11
|
+
include ActiveRecord::Metal::Postgresql::Etest
|
12
|
+
include ActiveRecord::Metal::Postgresql::Import::Etest
|
13
|
+
include ActiveRecord::Metal::Postgresql::Conversions::Etest
|
14
|
+
include ActiveRecord::Metal::Postgresql::Queries::Etest
|
15
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
|
3
|
+
require "bundler"
|
4
|
+
Bundler.setup(:test)
|
5
|
+
require "simplecov"
|
6
|
+
|
7
|
+
require "etest-unit"
|
8
|
+
require "active_record/metal"
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(
|
11
|
+
:adapter => "postgresql",
|
12
|
+
:database => "activerecord_metal"
|
13
|
+
)
|
14
|
+
|
15
|
+
metal = ActiveRecord::Metal.new
|
16
|
+
metal.ask "DROP TABLE IF EXISTS alloys"
|
17
|
+
metal.ask <<-SQL
|
18
|
+
CREATE TABLE alloys(
|
19
|
+
id SERIAL PRIMARY KEY,
|
20
|
+
num INTEGER,
|
21
|
+
num2 INTEGER,
|
22
|
+
str1 VARCHAR,
|
23
|
+
hsh hstore
|
24
|
+
)
|
25
|
+
SQL
|
26
|
+
|
27
|
+
class Alloy < ActiveRecord::Base
|
28
|
+
end
|
29
|
+
|
30
|
+
require "logger"
|
31
|
+
|
32
|
+
ENV["ARM_ENV"] = "test"
|
33
|
+
|
34
|
+
# ActiveRecord::Base.logger = Logger.new File.open(File.expand_path("../../log/test.log", __FILE__), "w")
|
35
|
+
ActiveRecord::Base.logger = Logger.new STDERR
|
36
|
+
ActiveRecord::Base.logger.level = Logger::INFO
|
37
|
+
# ActiveRecord::Base.auto_explain_threshold_in_seconds = 0.010
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-metal
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- radiospiel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Build your tests alongside your code.
|
31
|
+
email: eno@radiospiel.org
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/active_record/metal/logging.rb
|
37
|
+
- lib/active_record/metal/postgresql/conversions/etest.rb
|
38
|
+
- lib/active_record/metal/postgresql/conversions.rb
|
39
|
+
- lib/active_record/metal/postgresql/etest.rb
|
40
|
+
- lib/active_record/metal/postgresql/import.rb
|
41
|
+
- lib/active_record/metal/postgresql/queries.rb
|
42
|
+
- lib/active_record/metal/postgresql.rb
|
43
|
+
- lib/active_record/metal/transaction.rb
|
44
|
+
- lib/active_record/metal/version.rb
|
45
|
+
- lib/active_record/metal.rb
|
46
|
+
- lib/activerecord-metal.rb
|
47
|
+
- README.md
|
48
|
+
- test/pg_test.rb
|
49
|
+
- test/test_helper.rb
|
50
|
+
homepage: http://github.com/radiospiel/etest
|
51
|
+
licenses: []
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
hash: -1973847718838922274
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.8.25
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Build your tests alongside your code.
|
77
|
+
test_files: []
|