simple-sql 0.3.5 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b08f546478e2802e501c22208f65ed6314caf518
4
- data.tar.gz: d6cb02a8df19e49a1c089dfc2582deb00d43aa2d
3
+ metadata.gz: 337ccb887e118d5d1220c0f3d169e7b5818d44d8
4
+ data.tar.gz: 3fc46560189f1cea77c830ed598b17ac2654eada
5
5
  SHA512:
6
- metadata.gz: f17b07ed8d17ede670b0df1f488ef01e642af28ecbd1f1e4e96d00b54993b14f50626593dc018baf7d9ed93bc7f68b2231b385ae292c723164347baa8b806005
7
- data.tar.gz: 33f1f2b749f2cad395d38eae9ee6700c53d1eac9b3037a8ec8e7e35fbbc4f9255711e0af0f12091015dfa32202f1f23e63bd8e38602c3f479d78420119bcd195
6
+ metadata.gz: 3f46132db9ebbaed89c5724e4688e1aabf16f951119fc4b9fc159435dffdfd0e270aed8b913c479b31f9eaf7a5b843744d71285016bf010ac7e937e6cdb77ee6
7
+ data.tar.gz: 7206041f5d04a31f9abfcbde92b61b8bf56d8e45b48547e62ef9a5915eeef7923787ae354684b6a7db419132d434ff4acb1f2fcc8ba267640a992a3c1a97aec4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple-sql (0.3.5)
4
+ simple-sql (0.3.6)
5
5
  pg (~> 0.20)
6
6
  pg_array_parser (~> 0)
7
7
 
data/README.md CHANGED
@@ -99,20 +99,28 @@ Simple::SQL.ask "SELECT id FROM users WHERE email=$1", "foo@local" # ret
99
99
  Simple::SQL.ask "SELECT id, email FROM users WHERE email=$?", "foo@local" # returns an array `[ <id>, <email> ]` (or `nil`)
100
100
  ```
101
101
 
102
- ### Simple::SQL.record/Simple::SQL.records: fetching hashes
102
+ ### Simple::SQL.ask/Simple::SQL.all: fetching hashes
103
103
 
104
104
  While `ask` and `all` convert each result row into an Array, sometimes you might want
105
- to use Hashes or similar objects instead. To do so, you use the record and records functions:
105
+ to use Hashes or similar objects instead. To do so, you use the `into:` keyword argument:
106
106
 
107
107
  # returns a single Hash (or nil)
108
- Simple::SQL.record("SELECT id FROM users")
108
+ Simple::SQL.ask("SELECT id FROM users", into: Hash)
109
109
 
110
110
  If you want the returned record to be in a structure which is not a Hash, you can use
111
111
  the `into: <klass>` option. The following would return an array of up to two `OpenStruct`
112
112
  objects:
113
113
 
114
114
  sql = "SELECT id, email FROM users WHERE id = ANY($1) LIMIT 1",
115
- Simple::SQL.records sql, [1,2,3], into: OpenStruct
115
+ Simple::SQL.all sql, [1,2,3], into: OpenStruct
116
+
117
+ This supports all target types that take a contructor which acceps Hash arguments.
118
+
119
+ It also supports a :struct argument, in which case simple-sql creates uses a Struct-class.
120
+ Struct classes are reused when possible, and are maintained by Simple::SQL.
121
+
122
+ sql = "SELECT id, email FROM users WHERE id = ANY($1) LIMIT 1",
123
+ Simple::SQL.all sql, [1,2,3], into: :struct
116
124
 
117
125
  ### Transaction support
118
126
 
@@ -161,3 +169,5 @@ strings containing the "`" character.
161
169
  1. `createdb simple-sql-test`
162
170
  2. `bundle install`
163
171
  3. `bin/rspec`
172
+
173
+ ## Test again
data/Rakefile CHANGED
@@ -1,6 +1,11 @@
1
1
  Dir.glob("tasks/*.rake").each { |r| import r }
2
2
 
3
- task :default do
3
+
4
+ task "test:prepare_db" do
5
+ sh "createdb simple-sql-test 2>&1 > /dev/null || true"
6
+ end
7
+
8
+ task default: "test:prepare_db" do
4
9
  sh "rspec"
5
10
  sh "USE_ACTIVE_RECORD=1 rspec"
6
11
  sh "rubocop -D"
data/config/database.yml CHANGED
@@ -2,7 +2,7 @@ defaults: &defaults
2
2
  adapter: postgresql
3
3
  encoding: utf8
4
4
  host: '127.0.0.1'
5
- username: postgres
5
+ # username: postgres
6
6
  pool: 5
7
7
  timeout: 5000
8
8
 
data/lib/simple/sql.rb CHANGED
@@ -11,6 +11,8 @@ require_relative "sql/reflection.rb"
11
11
  require_relative "sql/insert.rb"
12
12
  require_relative "sql/duplicate.rb"
13
13
 
14
+ # rubocop:disable Metrics/MethodLength
15
+
14
16
  module Simple
15
17
  # The Simple::SQL module
16
18
  module SQL
@@ -56,11 +58,9 @@ module Simple
56
58
  # # do something
57
59
  # end
58
60
 
59
- def all(sql, *args, &block)
60
- result = exec_logged(sql, *args)
61
- decoder = Decoder.new(result)
62
-
63
- enumerate(result, decoder, block)
61
+ def all(sql, *args, into: nil, &block)
62
+ result = exec_logged(sql, *args)
63
+ enumerate(result, into: into, &block)
64
64
  end
65
65
 
66
66
  # Runs a query and returns the first result row of a query.
@@ -71,38 +71,23 @@ module Simple
71
71
  # returns a number (or +nil+)
72
72
  # - <tt>Simple::SQL.ask "SELECT id, email FROM users WHERE email=$?", "foo@local"</tt>
73
73
  # returns an array <tt>[ <id>, <email> ]</tt> (or +nil+)
74
- def ask(sql, *args)
74
+ def ask(sql, *args, into: nil)
75
75
  catch(:ok) do
76
- all(sql, *args) { |row| throw :ok, row }
76
+ all(sql, *args, into: into) { |row| throw :ok, row }
77
77
  nil
78
78
  end
79
79
  end
80
80
 
81
- # Runs a query, with optional arguments, and returns the result as an
82
- # array of Hashes.
83
- #
84
- # Example:
85
- #
86
- # - <tt>Simple::SQL.records("SELECT id, email FROM users")</tt> returns an array of
87
- # hashes { id: id, email: email }
88
- #
89
- # Simple::SQL.records "SELECT id, email FROM users" do |record|
90
- # # do something
91
- # end
92
-
81
+ # [Deprecated] Runs a query, with optional arguments, and returns the
82
+ # result as an array of Hashes.
93
83
  def records(sql, *args, into: Hash, &block)
94
- result = exec_logged(sql, *args)
95
- decoder = Decoder.new(result, :record, into: into)
96
-
97
- enumerate(result, decoder, block)
84
+ all sql, *args, into: into, &block
98
85
  end
99
86
 
100
- # Runs a query and returns the first result row of a query as a Hash.
87
+ # [Deprecated] Runs a query and returns the first result row of a query
88
+ # as a Hash.
101
89
  def record(sql, *args, into: Hash)
102
- catch(:ok) do
103
- records(sql, *args, into: into) { |row| throw :ok, row }
104
- nil
105
- end
90
+ ask sql, *args, into: into
106
91
  end
107
92
 
108
93
  extend Forwardable
@@ -116,10 +101,12 @@ module Simple
116
101
  end
117
102
  end
118
103
 
119
- def enumerate(result, decoder, block)
104
+ def enumerate(result, into:, &block)
105
+ decoder = Decoder.new(result, into: into)
106
+
120
107
  if block
121
108
  result.each_row do |row|
122
- block.call decoder.decode(row)
109
+ yield decoder.decode(row)
123
110
  end
124
111
  self
125
112
  else
@@ -40,9 +40,14 @@ module Simple::SQL::Config
40
40
  abc = read_database_yml
41
41
  username, password, host, port, database = abc.values_at "username", "password", "host", "port", "database"
42
42
 
43
+ # raise username.inspect
43
44
  username_and_password = [username, password].compact.join(":")
44
45
  host_and_port = [host, port].compact.join(":")
45
- "postgres://#{username_and_password}@#{host_and_port}/#{database}"
46
+ if username_and_password != ""
47
+ "postgres://#{username_and_password}@#{host_and_port}/#{database}"
48
+ else
49
+ "postgres://#{host_and_port}/#{database}"
50
+ end
46
51
  end
47
52
 
48
53
  def read_database_yml
@@ -4,10 +4,12 @@ require "time"
4
4
  module Simple::SQL::Decoder
5
5
  extend self
6
6
 
7
- def new(result, mode = nil, into: nil)
8
- if mode == :record then Record.new(result, into: into)
7
+ def new(result, into:)
8
+ if into == Hash then HashRecord.new(result)
9
+ elsif into == :struct then StructRecord.new(result)
10
+ elsif into then Record.new(result, into: into)
9
11
  elsif result.nfields == 1 then SingleColumn.new(result)
10
- else MultiColumns.new(result)
12
+ else MultiColumns.new(result)
11
13
  end
12
14
  end
13
15
 
@@ -79,6 +81,18 @@ module Simple::SQL::Decoder
79
81
  end
80
82
  end
81
83
 
84
+ class Simple::SQL::Decoder::SingleColumn
85
+ def initialize(result)
86
+ typename = ::Simple::SQL.send(:resolve_type, result.ftype(0), result.fmod(0))
87
+ @field_type = typename.to_sym
88
+ end
89
+
90
+ def decode(row)
91
+ value = row.first
92
+ value && Simple::SQL::Decoder.decode_value(@field_type, value)
93
+ end
94
+ end
95
+
82
96
  class Simple::SQL::Decoder::MultiColumns
83
97
  def initialize(result)
84
98
  @field_types = 0.upto(result.fields.length - 1).map do |idx|
@@ -94,34 +108,41 @@ class Simple::SQL::Decoder::MultiColumns
94
108
  end
95
109
  end
96
110
 
97
- class Simple::SQL::Decoder::SingleColumn < Simple::SQL::Decoder::MultiColumns
111
+ class Simple::SQL::Decoder::HashRecord < Simple::SQL::Decoder::MultiColumns
98
112
  def initialize(result)
99
- super
100
- @field_type = @field_types.first
113
+ super(result)
114
+ @field_names = result.fields.map(&:to_sym)
101
115
  end
102
116
 
103
117
  def decode(row)
104
- value = row.first
105
- value && Simple::SQL::Decoder.decode_value(@field_type, value)
118
+ decoded_row = super(row)
119
+ Hash[@field_names.zip(decoded_row)]
106
120
  end
107
121
  end
108
122
 
109
- class Simple::SQL::Decoder::Record < Simple::SQL::Decoder::MultiColumns
123
+ class Simple::SQL::Decoder::Record < Simple::SQL::Decoder::HashRecord
110
124
  def initialize(result, into:)
111
125
  super(result)
112
-
113
126
  @into = into
114
- @result = result
115
- @field_names = @result.fields.map(&:to_sym)
127
+ end
128
+
129
+ def decode(row)
130
+ @into.new(super)
131
+ end
132
+ end
133
+
134
+ class Simple::SQL::Decoder::StructRecord < Simple::SQL::Decoder::MultiColumns
135
+ @@struct_cache = {}
136
+
137
+ def initialize(result)
138
+ super(result)
139
+
140
+ field_names = result.fields.map(&:to_sym)
141
+ @into = @@struct_cache[field_names] ||= Struct.new(*field_names)
116
142
  end
117
143
 
118
144
  def decode(row)
119
145
  decoded_row = super(row)
120
- hsh = Hash[@field_names.zip(decoded_row)]
121
- if @into && @into != Hash
122
- @into.new(hsh)
123
- else
124
- hsh
125
- end
146
+ @into.new(*decoded_row)
126
147
  end
127
148
  end
@@ -1,22 +1,27 @@
1
1
  # rubocop:disable Style/ClassVars
2
2
  # rubocop:disable Metrics/AbcSize
3
-
3
+ # rubocop:disable Metrics/LineLength
4
4
  module Simple
5
5
  module SQL
6
- def insert(table, records)
6
+ #
7
+ # - table_name - the name of the table
8
+ # - records - a single hash of attributes or an array of hashes of attributes
9
+ # - on_conflict - uses a postgres ON CONFLICT clause to ignore insert conflicts if true
10
+ #
11
+ def insert(table, records, on_conflict: nil)
7
12
  if records.is_a?(Hash)
8
- insert_many(table, [records]).first
13
+ insert_many(table, [records], on_conflict).first
9
14
  else
10
- insert_many table, records
15
+ insert_many(table, records, on_conflict)
11
16
  end
12
17
  end
13
18
 
14
19
  private
15
20
 
16
- def insert_many(table, records)
21
+ def insert_many(table, records, on_conflict)
17
22
  return [] if records.empty?
18
23
 
19
- inserter = Inserter.create(table_name: table.to_s, columns: records.first.keys)
24
+ inserter = Inserter.create(table_name: table.to_s, columns: records.first.keys, on_conflict: on_conflict)
20
25
  inserter.insert(records: records)
21
26
  end
22
27
 
@@ -25,15 +30,15 @@ module Simple
25
30
 
26
31
  @@inserters = {}
27
32
 
28
- def self.create(table_name:, columns:)
29
- @@inserters[[table_name, columns]] ||= new(table_name: table_name, columns: columns)
33
+ def self.create(table_name:, columns:, on_conflict:)
34
+ @@inserters[[table_name, columns, on_conflict]] ||= new(table_name: table_name, columns: columns, on_conflict: on_conflict)
30
35
  end
31
36
 
32
37
  #
33
38
  # - table_name - the name of the table
34
39
  # - columns - name of columns, as Array[String] or Array[Symbol]
35
40
  #
36
- def initialize(table_name:, columns:)
41
+ def initialize(table_name:, columns:, on_conflict:)
37
42
  @columns = columns
38
43
 
39
44
  cols = []
@@ -47,7 +52,19 @@ module Simple
47
52
  cols += timestamp_columns
48
53
  vals += timestamp_columns.map { "now()" }
49
54
 
50
- @sql = "INSERT INTO #{table_name} (#{cols.join(',')}) VALUES(#{vals.join(',')}) RETURNING id"
55
+ @sql = "INSERT INTO #{table_name} (#{cols.join(',')}) VALUES(#{vals.join(',')}) #{confict_handling(on_conflict)} RETURNING id"
56
+ end
57
+
58
+ CONFICT_HANDLING = {
59
+ nil => "",
60
+ :nothing => "ON CONFLICT DO NOTHING",
61
+ :ignore => "ON CONFLICT DO NOTHING"
62
+ }
63
+
64
+ def confict_handling(on_conflict)
65
+ CONFICT_HANDLING.fetch(on_conflict) do
66
+ raise(ArgumentError, "Invalid on_conflict value #{on_conflict.inspect}")
67
+ end
51
68
  end
52
69
 
53
70
  def insert(records:)
@@ -1,5 +1,5 @@
1
1
  module Simple
2
2
  module SQL
3
- VERSION = "0.3.5"
3
+ VERSION = "0.3.6"
4
4
  end
5
5
  end
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ describe "Simple::SQL.all into: argument" do
4
+ let!(:users) { 1.upto(USER_COUNT).map { create(:user) } }
5
+
6
+ it "calls the database" do
7
+ r = SQL.all("SELECT * FROM users", into: Hash)
8
+ expect(r).to be_a(Array)
9
+ expect(r.length).to eq(USER_COUNT)
10
+ expect(r.map(&:class).uniq).to eq([Hash])
11
+ end
12
+
13
+ it "returns an empty array when there is no match" do
14
+ r = SQL.all("SELECT * FROM users WHERE FALSE", into: Hash)
15
+ expect(r).to eq([])
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+
3
+ describe "Simple::SQL.ask into: argument" do
4
+ let!(:users) { 1.upto(USER_COUNT).map { create(:user) } }
5
+
6
+ it "calls the database" do
7
+ r = SQL.ask("SELECT COUNT(*) AS count FROM users", into: Hash)
8
+ expect(r).to eq({count: 2})
9
+ end
10
+
11
+ it "returns nil when there is no match" do
12
+ r = SQL.ask("SELECT * FROM users WHERE FALSE", into: Hash)
13
+ expect(r).to be_nil
14
+ end
15
+
16
+ it "returns a OpenStruct with into: OpenStruct" do
17
+ r = SQL.ask("SELECT COUNT(*) AS count FROM users", into: OpenStruct)
18
+ expect(r).to be_a(OpenStruct)
19
+ expect(r).to eq(OpenStruct.new(count: 2))
20
+ end
21
+
22
+ it "supports the into: option even with parameters" do
23
+ r = SQL.ask("SELECT $1::integer AS count FROM users", 2, into: OpenStruct)
24
+ expect(r).to be_a(OpenStruct)
25
+ expect(r).to eq(OpenStruct.new(count: 2))
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ describe "Simple::SQL.ask into: :struct" do
4
+ let!(:users) { 1.upto(USER_COUNT).map { create(:user) } }
5
+
6
+ it "calls the database" do
7
+ r = SQL.ask("SELECT COUNT(*) AS count FROM users", into: :struct)
8
+ expect(r.count).to eq(2)
9
+ expect(r.class.members).to eq([:count])
10
+ end
11
+
12
+ it "reuses the struct" do
13
+ r1 = SQL.ask("SELECT COUNT(*) AS count FROM users", into: :struct)
14
+ r2 = SQL.ask("SELECT COUNT(*) AS count FROM users", into: :struct)
15
+ expect(r1.class.object_id).to eq(r2.class.object_id)
16
+ end
17
+ end
@@ -3,7 +3,7 @@ require "spec_helper"
3
3
  describe "Simple::SQL::Config" do
4
4
  describe ".determine_url" do
5
5
  it "reads config/database.yml" do
6
- expect(SQL::Config.determine_url).to eq "postgres://postgres@127.0.0.1/simple-sql-test"
6
+ expect(SQL::Config.determine_url).to eq "postgres://127.0.0.1/simple-sql-test"
7
7
  end
8
8
  end
9
9
 
@@ -16,5 +16,40 @@ describe "Simple::SQL.insert" do
16
16
  expect(user.last_name).to eq("bar")
17
17
  expect(user.created_at).to be_a(Time)
18
18
  end
19
- end
20
19
 
20
+ describe 'confict handling' do
21
+ let!(:existing_user_id) { SQL.insert :users, {first_name: "foo", last_name: "bar"} }
22
+ let!(:total_users) { SQL.ask "SELECT count(*) FROM users" }
23
+
24
+ context 'when called with on_conflict: :ignore' do
25
+ it 'ignores the conflict and does not create a user' do
26
+ # Try to insert using an existing primary key ...
27
+ result = SQL.insert :users, {id: existing_user_id, first_name: "foo", last_name: "bar"}, on_conflict: :ignore
28
+ expect(result).to be_nil
29
+
30
+ expect(SQL.ask("SELECT count(*) FROM users")).to eq(total_users)
31
+ end
32
+ end
33
+
34
+ context 'when called with on_conflict: :nothing' do
35
+ it 'ignores the conflict and does not create a user' do
36
+ # Try to insert using an existing primary key ...
37
+ result = SQL.insert :users, {id: existing_user_id, first_name: "foo", last_name: "bar"}, on_conflict: :nothing
38
+ expect(result).to be_nil
39
+
40
+ expect(SQL.ask("SELECT count(*) FROM users")).to eq(total_users)
41
+ end
42
+ end
43
+
44
+ context 'when called with on_conflict: nil' do
45
+ it 'raises an error and does not create a user' do
46
+ # Try to insert using an existing primary key ...
47
+ expect {
48
+ SQL.insert :users, {id: existing_user_id, first_name: "foo", last_name: "bar"}, on_conflict: nil
49
+ }.to raise_error(PG::UniqueViolation)
50
+
51
+ expect(SQL.ask("SELECT count(*) FROM users")).to eq(total_users)
52
+ end
53
+ end
54
+ end
55
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - radiospiel
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-03-08 00:00:00.000000000 Z
12
+ date: 2018-04-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pg_array_parser
@@ -180,6 +180,9 @@ files:
180
180
  - log/.gitkeep
181
181
  - simple-sql.gemspec
182
182
  - spec/simple/sql/version_spec.rb
183
+ - spec/simple/sql_all_into_spec.rb
184
+ - spec/simple/sql_ask_into_spec.rb
185
+ - spec/simple/sql_ask_into_struct_spec.rb
183
186
  - spec/simple/sql_ask_spec.rb
184
187
  - spec/simple/sql_config_spec.rb
185
188
  - spec/simple/sql_conversion_spec.rb
@@ -220,6 +223,9 @@ specification_version: 4
220
223
  summary: SQL with a simple interface
221
224
  test_files:
222
225
  - spec/simple/sql/version_spec.rb
226
+ - spec/simple/sql_all_into_spec.rb
227
+ - spec/simple/sql_ask_into_spec.rb
228
+ - spec/simple/sql_ask_into_struct_spec.rb
223
229
  - spec/simple/sql_ask_spec.rb
224
230
  - spec/simple/sql_config_spec.rb
225
231
  - spec/simple/sql_conversion_spec.rb