activerecord-metal 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +31 -7
- data/lib/active_record/metal/logging.rb +12 -0
- data/lib/active_record/metal/postgresql/aggregate.rb +5 -0
- data/lib/active_record/metal/postgresql/conversions.rb +4 -0
- data/lib/active_record/metal/postgresql/exec.rb +49 -0
- data/lib/active_record/metal/postgresql/import.rb +2 -8
- data/lib/active_record/metal/postgresql/prepared_queries.rb +105 -0
- data/lib/active_record/metal/postgresql/queries.rb +3 -0
- data/lib/active_record/metal/postgresql.rb +5 -81
- data/lib/active_record/metal/version.rb +1 -1
- data/lib/active_record/metal.rb +7 -0
- data/test/pg_test.rb +1 -0
- data/test/test_helper.rb +1 -0
- metadata +6 -3
data/README.md
CHANGED
@@ -46,6 +46,26 @@ So there is support for queries.
|
|
46
46
|
# only what is the database's default; and Postgresql uses $1, $2, ...
|
47
47
|
metal.exec("SELECT id, name FROM users WHERE name=$1", "me")
|
48
48
|
|
49
|
+
## Usage - prepared queries
|
50
|
+
|
51
|
+
ActiveRecord::Metal uses prepared queries whenever a query uses parameters. They
|
52
|
+
are managed automatically behind your back. You can also explicitely prepare
|
53
|
+
queries:
|
54
|
+
|
55
|
+
prepared_query = metal.prepare(sql)
|
56
|
+
|
57
|
+
and use the prepared_query instead of the sql string.
|
58
|
+
|
59
|
+
metal.ask prepared_query, arg1, arg2, ...
|
60
|
+
|
61
|
+
To unprepare the query use
|
62
|
+
|
63
|
+
metal.unprepare(prepared_query)
|
64
|
+
metal.unprepare(sql)
|
65
|
+
|
66
|
+
**Note:** ActiveRecord::Metal currently does not automatically unprepare
|
67
|
+
queries.
|
68
|
+
|
49
69
|
## Usage - Mass import
|
50
70
|
|
51
71
|
# Mass imports for array records
|
@@ -73,14 +93,11 @@ So there is support for queries.
|
|
73
93
|
|
74
94
|
### How fast is the mass import?
|
75
95
|
|
76
|
-
The metal's importer is
|
96
|
+
The metal's importer is fast, ceause it just does importing data - that
|
97
|
+
means it does not fetch ids, it does not validate records, does not call
|
98
|
+
callbacks.
|
77
99
|
|
78
|
-
|
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)
|
100
|
+
This results in an impressive speedup compared to ActiveRecord::Base:
|
84
101
|
|
85
102
|
1.9.2 ~/projects/gem/activerecord-metal[master] > bundle exec script/console
|
86
103
|
Loaded /Users/eno/.irbrc
|
@@ -121,6 +138,13 @@ And documentation is still missing. Code coverage is good, though:
|
|
121
138
|
All Files (97.22% covered at 88.78 hits/line)
|
122
139
|
6 files in total. 288 relevant lines. 280 lines covered and 8 lines missed
|
123
140
|
|
141
|
+
## Hacking ActiveRecord::Metal
|
142
|
+
|
143
|
+
The following gives you a IRB console
|
144
|
+
|
145
|
+
bundle exec script/console
|
146
|
+
|
124
147
|
## License
|
125
148
|
|
126
149
|
The activerecord-metal gem is distributed under the terms of the Modified BSD License, see LICENSE.BSD for details.
|
150
|
+
|
@@ -40,4 +40,16 @@ module ActiveRecord::Metal::Logging
|
|
40
40
|
@benchmark_depth -= 1
|
41
41
|
log_benchmark severity, Time.now - started_at, msg
|
42
42
|
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def log_error(exception, query, *args)
|
47
|
+
unless args.empty?
|
48
|
+
args = "w/#{args.map(&:inspect).join(", ")}"
|
49
|
+
else
|
50
|
+
args = ""
|
51
|
+
end
|
52
|
+
|
53
|
+
ActiveRecord::Metal.logger.error "#{exception} on #{resolve_query(query)} #{args}"
|
54
|
+
end
|
43
55
|
end
|
@@ -72,6 +72,10 @@ module ActiveRecord::Metal::Postgresql::Conversions::Time
|
|
72
72
|
def _timestamptz(s)
|
73
73
|
T.parse(s)
|
74
74
|
end
|
75
|
+
|
76
|
+
def _interval(s)
|
77
|
+
raise "ActiveRecord::Metal does not yet support the type '_interval': #{s.inspect}"
|
78
|
+
end
|
75
79
|
end
|
76
80
|
|
77
81
|
module ActiveRecord::Metal::Postgresql::Conversions::Boolean
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveRecord::Metal::Postgresql::Exec
|
2
|
+
private
|
3
|
+
|
4
|
+
# -- raw queries ----------------------------------------------------
|
5
|
+
|
6
|
+
def exec_(sql)
|
7
|
+
pg_conn.exec(sql)
|
8
|
+
end
|
9
|
+
|
10
|
+
def exec_prepared(sym, *args)
|
11
|
+
args = args.map do |arg|
|
12
|
+
if arg.is_a?(Hash)
|
13
|
+
ActiveRecord::Metal::Postgresql::Conversions::HStore.escape(arg)
|
14
|
+
else
|
15
|
+
arg
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
pg_conn.exec_prepared(sym.to_s, args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ActiveRecord::Metal::Postgresql::Exec::Etest
|
24
|
+
include ActiveRecord::Metal::EtestBase
|
25
|
+
|
26
|
+
def test_error_on_prepare
|
27
|
+
assert_raise(PG::Error) {
|
28
|
+
metal.prepare "SELECT unknown_function(1)"
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_error_on_exec
|
33
|
+
assert_raise(PG::Error) {
|
34
|
+
metal.ask "SELECT unknown_function(1)"
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_error_on_unprepare
|
39
|
+
assert_raise(PG::Error) {
|
40
|
+
metal.unprepare "SELECT unknown_function(1)"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_error_on_exec_with_args
|
45
|
+
assert_raise(PG::Error) {
|
46
|
+
metal.ask "SELECT num FROM alloys WHERE unknown_function(id) > $1", 1
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
@@ -37,10 +37,7 @@ module ActiveRecord::Metal::Postgresql::Import
|
|
37
37
|
records.each do |record|
|
38
38
|
exec_prepared stmt, *record.values_at(*keys)
|
39
39
|
end
|
40
|
-
|
41
|
-
logger.warn "#{$!.class.name}: #{$!}"
|
42
|
-
raise
|
43
|
-
ensure
|
40
|
+
|
44
41
|
unprepare(stmt)
|
45
42
|
end
|
46
43
|
|
@@ -57,10 +54,7 @@ module ActiveRecord::Metal::Postgresql::Import
|
|
57
54
|
records.each do |record|
|
58
55
|
exec_prepared stmt, *record
|
59
56
|
end
|
60
|
-
|
61
|
-
logger.warn "#{$!.class.name}: #{$!}"
|
62
|
-
raise
|
63
|
-
ensure
|
57
|
+
|
64
58
|
unprepare(stmt)
|
65
59
|
end
|
66
60
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ActiveRecord::Metal::Postgresql::PreparedQueries
|
2
|
+
def prepare(sql)
|
3
|
+
prepared_statements[sql]
|
4
|
+
rescue PG::Error
|
5
|
+
log_error $!, sql
|
6
|
+
raise
|
7
|
+
end
|
8
|
+
|
9
|
+
def unprepare(query)
|
10
|
+
expect! query => [ Symbol, String ]
|
11
|
+
case query
|
12
|
+
when Symbol
|
13
|
+
exec_("DEALLOCATE PREPARE #{query}")
|
14
|
+
sql = prepared_statements_by_name.delete query
|
15
|
+
prepared_statements.delete sql
|
16
|
+
when String
|
17
|
+
name = prepared_statement_name(query)
|
18
|
+
exec_("DEALLOCATE PREPARE #{name}")
|
19
|
+
prepared_statements.delete query
|
20
|
+
prepared_statements_by_name.delete name
|
21
|
+
end
|
22
|
+
rescue PG::Error
|
23
|
+
log_error $!, query
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def prepared_statements_by_name
|
30
|
+
@prepared_statements_by_name ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepared_statements
|
34
|
+
@prepared_statements ||= Hash.new { |hsh, sql| hsh[sql] = _prepare(sql) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def resolve_query(query)
|
38
|
+
query.is_a?(Symbol) ? prepared_statements_by_name[query] : query
|
39
|
+
end
|
40
|
+
|
41
|
+
def _prepare(sql)
|
42
|
+
name = prepared_statement_name(sql)
|
43
|
+
pg_conn.prepare(name, sql)
|
44
|
+
name = name.to_sym
|
45
|
+
prepared_statements_by_name[name] = sql
|
46
|
+
name
|
47
|
+
end
|
48
|
+
|
49
|
+
# Name for a prepared statement. The name is derived from the SQL code, but is also
|
50
|
+
# specific for this ActiveRecord::Metal.instance.
|
51
|
+
def prepared_statement_name(sql)
|
52
|
+
key = "#{object_id}-#{sql}"
|
53
|
+
"pg_metal_#{Digest::MD5.hexdigest(key)}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module ActiveRecord::Metal::Postgresql::PreparedQueries::Etest
|
58
|
+
include ActiveRecord::Metal::EtestBase
|
59
|
+
|
60
|
+
def test_prepared_queries_housekeeping
|
61
|
+
# if we have two metal adapters working on the same connection,
|
62
|
+
# one must not affect the prepared queries of the other.
|
63
|
+
alloys = metal.ask "SELECT COUNT(*) FROM alloys WHERE id >= $1", 0
|
64
|
+
other_metal = ActiveRecord::Metal.new
|
65
|
+
expect! other_metal.ask("SELECT COUNT(*) FROM alloys WHERE id >= $1", 0) => alloys
|
66
|
+
expect! metal.ask("SELECT COUNT(*) FROM alloys WHERE id >= $1", 0) => alloys
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_prepared_query_fails_during_import
|
70
|
+
metal.ask "DELETE FROM alloys"
|
71
|
+
|
72
|
+
# If a prepared query fails, eg. during an import, the transaction
|
73
|
+
# will fail and cancelled. This also means that there is no longer
|
74
|
+
# a way to clean up the prepared query in the transaction, because
|
75
|
+
# this fails with a "transaction failed already" error.
|
76
|
+
query = metal.prepare "INSERT INTO alloys (id, num) VALUES($1, $1)"
|
77
|
+
|
78
|
+
assert_raise(PG::Error) {
|
79
|
+
records = [ [1,1], [1,1] ]
|
80
|
+
metal.import "alloys", records, :columns => [ "id", "num"]
|
81
|
+
}
|
82
|
+
|
83
|
+
# Note: after that point we can no longer do anything in this test.
|
84
|
+
# This is because a test is wrapped in a transaction, and this
|
85
|
+
# transaction is aborted.
|
86
|
+
# expect! metal.count("alloys") => 0
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_transaction_aborted
|
90
|
+
metal.ask "DELETE FROM alloys"
|
91
|
+
metal.transaction do
|
92
|
+
metal.ask "INSERT INTO alloys (id, num) VALUES($1, $1)", 1
|
93
|
+
|
94
|
+
# "duplicate key value violates unique constraint"
|
95
|
+
assert_raise(PG::Error) {
|
96
|
+
metal.ask "INSERT INTO alloys (id, num) VALUES($1, $1)", 1
|
97
|
+
}
|
98
|
+
|
99
|
+
# "current transaction is aborted"
|
100
|
+
assert_raise(PG::Error) {
|
101
|
+
metal.ask "INSERT INTO alloys (id, num) VALUES($1, $1)", 2
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -96,6 +96,9 @@ module ActiveRecord::Metal::Postgresql::Queries
|
|
96
96
|
end
|
97
97
|
|
98
98
|
ArrayWithTypeInfo.new rows, result.columns, result.types
|
99
|
+
rescue PG::Error
|
100
|
+
log_error $!, sql, *args
|
101
|
+
raise
|
99
102
|
ensure
|
100
103
|
log_benchmark :debug, Time.now - started_at,
|
101
104
|
"SQL: {{runtime}} %s %s" % [ sql.is_a?(Symbol) ? "[P]" : " ", resolve_query(sql) ]
|
@@ -1,84 +1,6 @@
|
|
1
1
|
require "digest/md5"
|
2
2
|
|
3
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
4
|
# -- initialisation -------------------------------------------------
|
83
5
|
|
84
6
|
attr :pg_types, :pg_conn
|
@@ -87,7 +9,7 @@ module ActiveRecord::Metal::Postgresql
|
|
87
9
|
@pg_conn = connection.instance_variable_get("@connection")
|
88
10
|
@pg_types = load_pg_types
|
89
11
|
|
90
|
-
unprepare_all
|
12
|
+
# unprepare_all
|
91
13
|
|
92
14
|
name, installed_version = exec("SELECT name, installed_version FROM pg_available_extensions WHERE name='hstore'").first
|
93
15
|
exec_ "CREATE EXTENSION IF NOT EXISTS hstore" unless installed_version
|
@@ -118,9 +40,11 @@ end
|
|
118
40
|
|
119
41
|
require_relative "postgresql/conversions"
|
120
42
|
require_relative "postgresql/queries"
|
43
|
+
require_relative "postgresql/prepared_queries"
|
44
|
+
require_relative "postgresql/exec"
|
121
45
|
require_relative "postgresql/import"
|
46
|
+
require_relative "postgresql/aggregate"
|
122
47
|
|
123
48
|
module ActiveRecord::Metal::Postgresql
|
124
|
-
include Queries
|
125
|
-
include Import
|
49
|
+
include Queries, PreparedQueries, Exec, Import, Aggregate
|
126
50
|
end
|
data/lib/active_record/metal.rb
CHANGED
@@ -34,6 +34,9 @@ class ActiveRecord::Metal
|
|
34
34
|
|
35
35
|
include Logging
|
36
36
|
extend Logging
|
37
|
+
|
38
|
+
def self.logger; Logging.logger; end
|
39
|
+
def self.logger=(logger); Logging.logger = logger; end
|
37
40
|
end
|
38
41
|
|
39
42
|
module ActiveRecord::Metal::EtestBase
|
@@ -51,6 +54,10 @@ module ActiveRecord::Metal::EtestBase
|
|
51
54
|
def metal
|
52
55
|
@metal ||= ActiveRecord::Metal.new
|
53
56
|
end
|
57
|
+
|
58
|
+
def count(table)
|
59
|
+
metal.ask("SELECT COUNT(*) FROM #{table}")
|
60
|
+
end
|
54
61
|
end
|
55
62
|
|
56
63
|
module ActiveRecord::Metal::Etest
|
data/test/pg_test.rb
CHANGED
@@ -12,4 +12,5 @@ class PostgresTest < Test::Unit::TestCase
|
|
12
12
|
include ActiveRecord::Metal::Postgresql::Import::Etest
|
13
13
|
include ActiveRecord::Metal::Postgresql::Conversions::Etest
|
14
14
|
include ActiveRecord::Metal::Postgresql::Queries::Etest
|
15
|
+
include ActiveRecord::Metal::Postgresql::Exec::Etest
|
15
16
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-metal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-07-
|
12
|
+
date: 2013-07-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -34,10 +34,13 @@ extensions: []
|
|
34
34
|
extra_rdoc_files: []
|
35
35
|
files:
|
36
36
|
- lib/active_record/metal/logging.rb
|
37
|
+
- lib/active_record/metal/postgresql/aggregate.rb
|
37
38
|
- lib/active_record/metal/postgresql/conversions/etest.rb
|
38
39
|
- lib/active_record/metal/postgresql/conversions.rb
|
39
40
|
- lib/active_record/metal/postgresql/etest.rb
|
41
|
+
- lib/active_record/metal/postgresql/exec.rb
|
40
42
|
- lib/active_record/metal/postgresql/import.rb
|
43
|
+
- lib/active_record/metal/postgresql/prepared_queries.rb
|
41
44
|
- lib/active_record/metal/postgresql/queries.rb
|
42
45
|
- lib/active_record/metal/postgresql.rb
|
43
46
|
- lib/active_record/metal/transaction.rb
|
@@ -61,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
64
|
version: '0'
|
62
65
|
segments:
|
63
66
|
- 0
|
64
|
-
hash:
|
67
|
+
hash: 89526488637997823
|
65
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
69
|
none: false
|
67
70
|
requirements:
|