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 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: