quote-sql 0.0.4 โ†’ 0.0.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
  SHA256:
3
- metadata.gz: 70f787b04560e3b297b4a392eb72c0e85be57e349a0533366056fe485f339401
4
- data.tar.gz: ad8a2e347c97e12b78a264e67d0e12649e838127a0079d8ff96bf5ac51764b40
3
+ metadata.gz: 7e308b3aef586983a61155c9ba3be6c39c2397fa5fbae069ab45522e51681caa
4
+ data.tar.gz: 6a3945e1a4cfc16fed91c46216d1a10c5a69e85a6b3a0df10db45a9f162c07eb
5
5
  SHA512:
6
- metadata.gz: 7d2a6819ad0d7fc752a70a04efb48c7583759a1b6cd07eb93c0bacd5deec9845b4abf578ae902c94aaf68ba4cf41720b1f05aea8dc6950b468c8cc8f42c34c42
7
- data.tar.gz: e84a311154ef05f71b8af81c17cac1b4923f067fb48556f501aa075ab8fca1c112d494c8b0b58f1c8350d7d7467c10ba99f74591755beb206415dbe78756c7ff
6
+ metadata.gz: 3ce751f52d51a6b7b989062673f9c849b5a8b44e5e4227f0f453290d41b20856b45f0d0bbddd6cea6f5256405b313d82367a27fdd2381dd2b37f27c3ac3442e6
7
+ data.tar.gz: 2656c2cbacc7375fd22f2148d4a929fbba1822c97b61467bdf5794c7bcf78b75d5ba4a9bfb90a6f01b282479bad95c77d354ca2211a6f3561a6eaa60e9d90db4
data/README.md CHANGED
@@ -1,21 +1,26 @@
1
1
  # QuoteSql - Tool to build and run SQL queries easier
2
- I've built this library as an addition to ActiveRecord and Arel, however you can use it with any sql database and plain Ruby.
3
- However currently it is just used with PostgreSQL.
4
-
5
2
  Creating SQL queries and proper quoting becomes complicated especially when you need advanced queries.
6
3
 
7
- I created this library while coding for different projects, and had lots of Heredoc SQL queries, which pretty quickly becomes the kind of:
8
- > When I wrote these lines of code, just me and God knew what they mean. Now its just God.
4
+ I created this library while coding for different projects, and had lots of Heredoc SQL queries, which pretty quickly became unreadable.
5
+
6
+ With QuoteSql you segment SQL Queries in readable junks, which can be individually tested and then combine them to the final query.
7
+ When us use RoR, you can combine queries or get the output with fields other than `pick` or `pluck`
9
8
 
10
- My strategy is to segment SQL Queries in readable junks, which can be individually tested and then combine their sql to the final query.
9
+ Please have a look at the *unfinished* documentation below or run `QuoteSql.test` in a Ruby console
11
10
 
12
- QuoteSql is used in production, but is still bleeding edge - and there is not a fully sync between doc and code.
11
+ If you think QuoteSql is interesting but needs extension, let's chat!
13
12
 
14
- If you think QuoteSql is interesting, let's chat!
15
- Also if you have problems using it, just drop me a note.
13
+ If you run into problems, drop me a note.
16
14
 
17
15
  Best Martin
18
16
 
17
+ ## Caveats & Notes
18
+ - Currently its just built for Ruby 3, if you need Ruby 2, let me know.
19
+ - QuoteSql is used in production, but is still bleeding edge - and there is not a fully sync between doc and code.
20
+ - Just for my examples and in the docs, I'm using for Yajl for JSON parsing, and changed in my environments the standard parse output to *symbolized keys*.
21
+ - I've built this library as an addition to ActiveRecord and Arel, however you can use it with any sql database and plain Ruby.
22
+ - It is currently built for PostgreSQL only. If you want to use other DBs, please contribute your code!
23
+
19
24
  ## Examples
20
25
  ### Simple quoting
21
26
  `QuoteSql.new("SELECT %field").quote(field: "abc").to_sql`
@@ -29,11 +34,12 @@ Best Martin
29
34
  => SELECT first_name, last_name FROM users LIMIT 10
30
35
 
31
36
  ### Quoting of columns and table from a model - or an object responding to table_name and column_names or columns
32
- `QuoteSql.new("SELECT %columns FROM %table_name").quote(table: User).to_sql`
37
+ `QuoteSql.new("SELECT %columns FROM %table").quote(table: User).to_sql`
33
38
  => SELECT "id",firstname","lastname",... FROM "users"
39
+
34
40
  ### Injecting raw sql in a query
35
- `QuoteSql.new("SELECT a,b,%raw FROM table").quote(raw: "jsonb_build_object('a', 1)").to_sql`
36
- => SELECT "a,b,jsonb_build_object('a', 1) FROM table
41
+ `QuoteSql.new("SELECT a,b,%raw FROM my_table").quote(raw: "jsonb_build_object('a', 1)").to_sql`
42
+ => SELECT "a,b,jsonb_build_object('a', 1) FROM my_table
37
43
 
38
44
  ### Injecting ActiveRecord, Arel.sql or QuoteSql
39
45
  `QuoteSql.new("SELECT %column_names FROM (%any_name) a").
@@ -52,15 +58,34 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
52
58
  ON CONFLICT ("id") DO NOTHING
53
59
 
54
60
  ### Columns from a list
55
- `QuoteSql.new("SELECT %columns").quote(columns: [:a, :"b.c", c: {d: field}]).to_sql`
56
- => SELECT "a","b"."c",jsonb_build_object('d', field) AS c
61
+ `QuoteSql.new("SELECT %columns").quote(columns: [:a, "b.c", d: {e: field}]).to_sql`
62
+ => SELECT "a","b"."c",jsonb_build_object('e', field) AS d
63
+
64
+ `QuoteSql.new("SELECT %columns").quote(columns: [:a, "b.c", d: {e: field, nil: false}]).to_sql`
65
+ => SELECT "a","b"."c",jsonb_strip_nulls(jsonb_build_object('e', 1)) AS d
57
66
 
58
- `QuoteSql.new("SELECT %columns").quote(columns: [:a, :"b.c", c: {d: field, nil: false}]).to_sql`
59
- => SELECT "a","b"."c",jsonb_strip_nulls(jsonb_build_object('d', 1)) AS c
67
+ ## Executing
68
+ ### Getting the results
69
+ `QuoteSql.new('SELECT %x AS a').quote(x: 1).result`
70
+ => [{:a=>1}]
71
+
72
+ ### Binds
73
+ You can use binds ($1, $2, ...) in the SQL and add arguments to the result call
74
+ `QuoteSql.new('SELECT $1 AS a').result(1)`
75
+
76
+ #### using JSON
77
+
78
+ v = {a: 1, b: "foo", c: true}
79
+ QuoteSQL(%q{SELECT * FROM %x_json}, x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"}).result(v.to_json)
80
+
81
+ => SELECT * FROM json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean) => [{a: 1, b: "foo", c: true}]
82
+
83
+ Insert fom json
84
+
85
+ v = {a: 1, b: "foo", c: true}
86
+ QuoteSql.new("INSERT INTO table (%x_columns) SELECT * FROM %x_json").quote({:x_json=>1}).result(v.to_json)
60
87
 
61
88
 
62
- ### Execution of a query
63
- `QuoteSql.new("Select 1 as abc").result` => [{:abc=>1}]
64
89
 
65
90
 
66
91
  ## Substitution of mixins with quoted values
@@ -71,24 +96,14 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
71
96
  **Caution! You need to take care, no protection against infinite recursion **
72
97
 
73
98
  ### Special mixins
74
- - `%table` | `%table_name` | `%table_names`
75
- - `%column` | `%columns` | `%column_names`
99
+ - `%table` +String+, +ActiveRecord::Base+, Object responding to #to_sql, and +Array+ of these
100
+ - `%columns` +Array+ of +String+, +Hash+ keys: AS +Symbol+, +String+. fallback: 1) %casts keys, 2) %table.columns
101
+ - `%casts` +Hash+ keys: column name, values: Cast e.g. "text", "integer"
76
102
  - `%ident` | `%constraint` | `%constraints` quoting for database columns
77
103
  - `%raw` | `%sql` inserting raw SQL
78
- - `%value` | `%values` creates value section for e.g. insert
79
- - In the right order
80
- - Single value => (2)
81
- - +Array+ => (column, column, column) n.b. has to be the correct order
82
- - +Array+ of +Array+ => (...),(...),(...),...
83
- - if the columns option is given (or implicitely by setting table)
84
- - +Hash+ values are ordered according to the columns option, missing values are replaced by `DEFAULT`
85
- - +Array+ of +Hash+ multiple record insert
86
- - `%bind` is replaced with the current bind sequence.
87
- Without appended number the first %bind => $1, the second => $2 etc.
88
- - %bind\\d+ => $+Integer+ e.g. `%bind7` => $7
89
- - `%bind__text` => $1 and it is registered as text - this is used in prepared statements (TO BE IMPLEMENTED)
90
- - `%key_bind__text` => $1 and it is registered as text when using +Hash+ in the execute
91
- $1 will be mapped to the key's value in the +Hash+ TODO
104
+ - `%values` creates the value section for INSERT `INSERT INTO foo (a,b) %values`
105
+ - `%x_values` creates the value secion for FROM `SELECT column1, column2, column3 FROM %x_values`
106
+ - `%x_json` creates `json_for_recordset(JSON) x (CASTS)`. "x" can be any other identifier, you need to define the casts e.g. `quotes(x_json: {a: "a", b: 1}, x_casts: {a: :text, b: :integer)`
92
107
 
93
108
  All can be preceded by additional letters and underscore e.g. `%foo_bar_column`
94
109
 
@@ -108,16 +123,27 @@ with optional array dimension
108
123
  - When the value responds to :to_sql or is a +Arel::Nodes::SqlLiteral+ its added as raw SQL
109
124
  - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
110
125
 
111
- ### Special quoting columns
112
- - +String+ or +Symbol+ without a dot e.g. :firstname => "firstname"
113
- - +String+ or +Symbol+ containing a dot e.g. "users.firstname" or => "users"."firstname"
126
+ ### Special quoting for %columns
127
+
128
+ `QuoteSql.new("SELECT %columns FROM %table, other_table").quote(columns: ["a", "other_table.a", :a ], table: "my_table")`
129
+ => SELECT "a", "other_table"."a", "my_table"."a" from "my_table", "other_table"
130
+
131
+ - +String+ without a dot e.g. "firstname" => "firstname"
132
+ - +String+ containing a dot e.g. "users.firstname" or => "users"."firstname"
133
+ - +Symbol+ prepended with table from table: quote if present.
134
+ - +Proc+ is called in the current context
135
+ - +QuoteSql::Raw+ or +Arel::Nodes::SqlLiteral+ are injected as is
136
+ - Object responding to #to_sql is called and injected
114
137
  - +Array+
115
- - +String+ and +Symbols+ see above
116
138
  - +Hash+ see below
117
- - +Hash+ or within the +Array+
118
- - +Symbol+ value will become the column name e.g. {table: :column} => "table"."column"
119
- - +String+ value will become the expression, the key the AS {result: "SUM(*)"} => SUM(*) AS result
120
- - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
139
+ - other see above
140
+ - +Hash+
141
+ - keys become the "AS"
142
+ - values
143
+ - +Hash+, +Array+ casted as JSONB
144
+ - others see above
145
+
146
+
121
147
 
122
148
  ## Shortcuts and functions
123
149
  - `QuoteSQL("select %abc", abc: 1)` == `QuoteSql.new("select %abc").quote(abc: 1)`
@@ -129,8 +155,8 @@ If you have pg_format installed you can get the resulting query inspected:
129
155
  `QuoteSql.new("select %abc").quote(abc: 1).dsql`
130
156
 
131
157
  # Test
132
- Minimal tests you can run by
133
- `QuoteSql.test.all`
158
+ Currently there are just minimal tests
159
+ run `QuoteSql.test`
134
160
  You can find them in /lib/quote_sql/test.rb
135
161
 
136
162
  ## Installing
@@ -143,8 +169,15 @@ Add this to config/initializers/quote_sql.rb
143
169
 
144
170
  ActiveSupport.on_load(:active_record) do
145
171
  require 'quote_sql'
146
- QuoteSql.db_connector = ActiveRecord::Base
172
+
173
+ # if you want to execute from Strings
174
+ # e.g. "select %a".quote_sql(a: 1).result
147
175
  String.include QuoteSql::Extension
176
+
177
+ # if you use Active Record
178
+ QuoteSql.db_connector = ActiveRecord::Base
179
+ # if you want to execute from a Model
180
+ # e.g. User.select("name, %a").quote_sql(a: 1).result
148
181
  ActiveRecord::Relation.include QuoteSql::Extension
149
182
  end
150
183
 
@@ -37,9 +37,13 @@ class QuoteSql
37
37
  def ident_columns(name = nil)
38
38
  item = columns(name || self.name)
39
39
  unless item
40
- table = self.table(name || self.name)
41
- raise ArgumntError, "No columns or table given" unless table&.respond_to? :column_names
42
- item = table.column_names
40
+ unless item = casts(name || self.name)&.keys
41
+ if (table = self.table(name || self.name))&.respond_to? :column_names
42
+ item = table.column_names
43
+ else
44
+ raise ArgumntError, "No columns, casts or table given for #{name}" unless table&.respond_to? :column_names
45
+ end
46
+ end
43
47
  end
44
48
  if item.is_a?(Array)
45
49
  if item.all? { _1.respond_to?(:name) }
@@ -143,11 +147,15 @@ class QuoteSql
143
147
  casts = self.casts(name)
144
148
  columns = self.columns(name) || casts&.keys
145
149
  column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
146
- item = [item].flatten.compact.as_json.map{_1.slice(*columns.map(&:to_s))}
147
- Raw.sql "json_to_recordset('#{item.to_json.gsub(/'/,"''")}') AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
150
+ if item.is_a? Integer
151
+ rv = "$#{item}"
152
+ else
153
+ item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
154
+ rv = "'#{item.to_json.gsub(/'/, "''")}'"
155
+ end
156
+ Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
148
157
  end
149
158
 
150
-
151
159
  def data_values(item = @quotable)
152
160
  item = Array(item).compact
153
161
  column_names = columns(name)
@@ -1,59 +1,5 @@
1
1
  class QuoteSql::Test
2
-
3
- def all
4
- @success = []
5
- @fail = []
6
- private_methods(false).grep(/^test_/).each { run(_1, true) }
7
- @success.each { STDOUT.puts(*_1, nil) }
8
- @fail.each { STDOUT.puts(*_1, nil) }
9
- puts
10
- end
11
-
12
- def run(name, all = false)
13
- name = name.to_s.sub(/^test_/, "")
14
- rv = ["๐Ÿงช #{name}"]
15
- @expected = nil
16
- @test = send("test_#{name}")
17
- if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
18
- tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
19
- columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
20
- rv += [@test.original, { **tables, **columns, **@test.quotes }.inspect, "๐ŸŽฏ #{expected}", "โœ… #{sql}"]
21
- @success << rv if @success
22
- else
23
- rv += [@test.inspect, "๐ŸŽฏ #{expected}", "โŒ #{sql}"]
24
- @fail << rv if @fail
25
- end
26
- rescue => exc
27
- rv += [@test.inspect, "๐ŸŽฏ #{expected}", "โŒ #{sql}", exc.message]
28
- @fail << rv if @fail
29
- ensure
30
- STDOUT.puts(*rv) unless @fail or @success
31
- end
32
-
33
- def expected(v = nil)
34
- @expected ||= v
35
- end
36
-
37
- def sql
38
- @test.to_sql
39
- end
40
-
41
- class PseudoActiveRecord
42
- def self.table_name
43
- "pseudo_active_records"
44
- end
45
-
46
- def self.column_names
47
- %w(id column1 column2)
48
- end
49
-
50
- def to_qsl
51
- "SELECT * FROM #{self.class.table_name}"
52
- end
53
- end
54
-
55
2
  private
56
-
57
3
  def test_columns
58
4
  expected <<~SQL
59
5
  SELECT x, "a", "b", "c", "d"
@@ -103,14 +49,14 @@ class QuoteSql::Test
103
49
  )
104
50
  end
105
51
 
106
- def test_binds
107
- expected <<~SQL
108
- SELECT $1, $2::UUID, $1 AS get_bind_1_again FROM "my_table"
109
- SQL
110
- QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table").quote(
111
- table: "my_table"
112
- )
113
- end
52
+ # def test_binds
53
+ # expected <<~SQL
54
+ # SELECT $1, $2::UUID, $1 AS get_bind_1_again FROM "my_table"
55
+ # SQL
56
+ # QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table").quote(
57
+ # table: "my_table"
58
+ # )
59
+ # end
114
60
 
115
61
  def test_from_values_array
116
62
  expected <<~SQL
@@ -189,6 +135,20 @@ class QuoteSql::Test
189
135
  "INSERT INTO users (name, color) SELECT * from %x_json".quote_sql(x_casts: {name: "text", color: "text"}, x_json:)
190
136
  end
191
137
 
138
+ def test_from_json_bind
139
+ expected <<~SQL
140
+ Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
141
+ SQL
142
+ QuoteSQL("Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
143
+ end
144
+
145
+ def test_insert_json_bind
146
+ expected <<~SQL
147
+ INSERT INTO table ("a","b","c") Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
148
+ SQL
149
+ QuoteSQL("INSERT INTO table (%x_columns) Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
150
+ end
151
+
192
152
  # def test_q3
193
153
  # expected Arel.sql(<<-SQL)
194
154
  # INSERT INTO "responses" ("id","type","task_id","index","data","parts","value","created_at","updated_at")
@@ -212,4 +172,60 @@ class QuoteSql::Test
212
172
  # )
213
173
  # end
214
174
 
175
+
176
+ public
177
+
178
+ def all
179
+ @success = []
180
+ @fail = []
181
+ private_methods(false).grep(/^test_/).each { run(_1, true) }
182
+ @success.each { STDOUT.puts(*_1, nil) }
183
+ @fail.each { STDOUT.puts(*_1, nil) }
184
+ puts
185
+ end
186
+
187
+ def run(name, all = false)
188
+ name = name.to_s.sub(/^test_/, "")
189
+ rv = ["๐Ÿงช #{name}"]
190
+ @expected = nil
191
+ @test = send("test_#{name}")
192
+ if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
193
+ tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
194
+ columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
195
+ rv += [
196
+ "QuoteSql.new(\"#{@test.original}\").quote(#{{**tables, **columns, **@test.quotes }.inspect}).to_sql", "๐ŸŽฏ #{expected}", "โœ… #{sql}"]
197
+ @success << rv if @success
198
+ else
199
+ rv += [@test.inspect, "๐ŸŽฏ #{expected}", "โŒ #{sql}"]
200
+ @fail << rv if @fail
201
+ end
202
+ rescue => exc
203
+ rv += [@test.inspect, "๐ŸŽฏ #{expected}", "โŒ #{sql}", exc.message]
204
+ @fail << rv if @fail
205
+ ensure
206
+ STDOUT.puts(*rv) unless @fail or @success
207
+ end
208
+
209
+ def expected(v = nil)
210
+ @expected ||= v
211
+ end
212
+
213
+ def sql
214
+ @test.to_sql
215
+ end
216
+
217
+ class PseudoActiveRecord
218
+ def self.table_name
219
+ "pseudo_active_records"
220
+ end
221
+
222
+ def self.column_names
223
+ %w(id column1 column2)
224
+ end
225
+
226
+ def to_qsl
227
+ "SELECT * FROM #{self.class.table_name}"
228
+ end
229
+ end
230
+
215
231
  end
@@ -1,3 +1,3 @@
1
1
  class QuoteSql
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.6"
3
3
  end
data/lib/quote_sql.rb CHANGED
@@ -82,14 +82,14 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
82
82
  @sql
83
83
  end
84
84
 
85
- def result(binds = [], prepare: false, async: false)
85
+ def result(*binds, prepare: false, async: false)
86
86
  sql = to_sql
87
- if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max + 1 != binds.length
87
+ if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max != binds.length
88
88
  raise ArgumentError, "Wrong number of binds"
89
89
  end
90
90
  _exec(sql, binds, prepare: false, async: false)
91
91
  rescue => exc
92
- STDERR.puts exc.sql
92
+ STDERR.puts exc.inspect, self.inspect
93
93
  raise exc
94
94
  end
95
95
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quote-sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Kufner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-26 00:00:00.000000000 Z
11
+ date: 2024-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: niceql