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 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 faster than ActiveRecord::Base's, because:
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
- - 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)
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
@@ -0,0 +1,5 @@
1
+ module ActiveRecord::Metal::Postgresql::Aggregate
2
+ def count(table_name)
3
+ ask "SELECT COUNT(*) FROM #{table_name}"
4
+ end
5
+ 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
- rescue
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
- rescue
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
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord; end
2
2
 
3
3
  class ActiveRecord::Metal
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -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
@@ -3,6 +3,7 @@ $:.unshift File.expand_path("../../lib", __FILE__)
3
3
  require "bundler"
4
4
  Bundler.setup(:test)
5
5
  require "simplecov"
6
+ SimpleCov.start
6
7
 
7
8
  require "etest-unit"
8
9
  require "active_record/metal"
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.0
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-18 00:00:00.000000000 Z
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: -1973847718838922274
67
+ hash: 89526488637997823
65
68
  required_rubygems_version: !ruby/object:Gem::Requirement
66
69
  none: false
67
70
  requirements: