activerecord-metal 0.1.0 → 0.1.2
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 +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:
|