simple-sql 0.3.5 → 0.3.6

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