quote-sql 0.0.5 → 0.0.7

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: cb47e85b4c7c4e1f945748295390c9e1bd49b7e0ba5291e0f7310058f6f91ddc
4
- data.tar.gz: 0f5bde1afc31eb573bb3e24727b530bf5909048284114d45c56c1b24a63e97aa
3
+ metadata.gz: cfe070af9a81f751ed10ef8ace30bf5e44a3efb54cff04f2eb5cc137b7fbc0f1
4
+ data.tar.gz: 8c8ec59ca80a8c72ceee6567da4beb5f79c96a63e16d945694fed26ea695feca
5
5
  SHA512:
6
- metadata.gz: 6596b9bb50ee373030e401dcb9d0067ae31857b6467fdd7b0341e21b3dcbe8fb0e78714ec24f56615618bbf965849307474a4030978e6c98d2d728166e235151
7
- data.tar.gz: f38366de3e2227835fd11f512a9c9d2a99d130d896a17f6b0a58788dd8ed7ebd13f46682992b762e2d50f09716c3bd5654d525dc4b03f350d3a52cc6b19b322e
6
+ metadata.gz: 03d69b9b3256cd22762844dc98c31ce2223c673079488d7175078dd82406d8031673368871a2a920ecda17ea68beb79c87f3208d56b1e770c4ba15583fafc648
7
+ data.tar.gz: b6d9f335cdcaf0e597f3118090451059220ebd9e540eda483f546301ad148bfebc35150a73a72c6dc54a39f537e8aa9b854970a975218226a26af3ed6add75c0
data/README.md CHANGED
@@ -1,29 +1,32 @@
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`
8
+
9
+ Please have a look at the *unfinished* documentation below or run `QuoteSql.test` in a Ruby console
9
10
 
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.
11
+ If you think QuoteSql is interesting but needs extension, let's chat!
11
12
 
12
- If you think QuoteSql is interesting, let's chat!
13
- Also if you have problems using it, just drop me a note.
13
+ If you run into problems, drop me a note.
14
14
 
15
15
  Best Martin
16
16
 
17
17
  ## Caveats & Notes
18
+ - Currently its just built for Ruby 3, if you need Ruby 2, let me know.
18
19
  - QuoteSql is used in production, but is still bleeding edge - and there is not a fully sync between doc and code.
19
- - 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*.
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!
20
23
 
21
24
  ## Examples
22
25
  ### Simple quoting
23
26
  `QuoteSql.new("SELECT %field").quote(field: "abc").to_sql`
24
27
  => SELECT 'abc'
25
28
 
26
- `QuoteSql.new("SELECT %field__text").quote(field__text: 9).to_sql`
29
+ `QuoteSql.new("SELECT %field::TEXT").quote(field: 9).to_sql`
27
30
  => SELECT 9::TEXT
28
31
 
29
32
  ### Rails models
@@ -31,11 +34,12 @@ Best Martin
31
34
  => SELECT first_name, last_name FROM users LIMIT 10
32
35
 
33
36
  ### Quoting of columns and table from a model - or an object responding to table_name and column_names or columns
34
- `QuoteSql.new("SELECT %columns FROM %table_name").quote(table: User).to_sql`
37
+ `QuoteSql.new("SELECT %columns FROM %table").quote(table: User).to_sql`
35
38
  => SELECT "id",firstname","lastname",... FROM "users"
39
+
36
40
  ### Injecting raw sql in a query
37
- `QuoteSql.new("SELECT a,b,%raw FROM table").quote(raw: "jsonb_build_object('a', 1)").to_sql`
38
- => 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
39
43
 
40
44
  ### Injecting ActiveRecord, Arel.sql or QuoteSql
41
45
  `QuoteSql.new("SELECT %column_names FROM (%any_name) a").
@@ -54,15 +58,32 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
54
58
  ON CONFLICT ("id") DO NOTHING
55
59
 
56
60
  ### Columns from a list
57
- `QuoteSql.new("SELECT %columns").quote(columns: [:a, :"b.c", c: {d: field}]).to_sql`
58
- => SELECT "a","b"."c",jsonb_build_object('d', field) AS c
61
+ `QuoteSql.new("SELECT %columns FROM %table").quote(table: "foo", columns: [:a, "b", "foo.c", {d: :e}]).to_sql`
62
+ => SELECT "foo"."a","b"."foo"."c", "foo"."e" AS d
59
63
 
60
- `QuoteSql.new("SELECT %columns").quote(columns: [:a, :"b.c", c: {d: field, nil: false}]).to_sql`
61
- => SELECT "a","b"."c",jsonb_strip_nulls(jsonb_build_object('d', 1)) AS c
64
+ ## Executing
65
+ ### Getting the results
66
+ `QuoteSql.new('SELECT %x AS a').quote(x: 1).result`
67
+ => [{:a=>1}]
68
+
69
+ ### Binds
70
+ You can use binds ($1, $2, ...) in the SQL and add arguments to the result call
71
+ `QuoteSql.new('SELECT $1 AS a').result(1)`
72
+ => [{:a=>1}]
73
+
74
+ #### using JSON
75
+
76
+ v = {a: 1, b: "foo", c: true}
77
+ QuoteSQL(%q{SELECT * FROM %x_json}, x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"}).result(v.to_json)
78
+
79
+ => SELECT * FROM json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean) => [{a: 1, b: "foo", c: true}]
80
+
81
+ Insert fom json
82
+
83
+ v = {a: 1, b: "foo", c: true}
84
+ QuoteSql.new("INSERT INTO table (%x_columns) SELECT * FROM %x_json").quote({:x_json=>1}).result(v.to_json)
62
85
 
63
86
 
64
- ### Execution of a query
65
- `QuoteSql.new("Select 1 as abc").result` => [{:abc=>1}]
66
87
 
67
88
 
68
89
  ## Substitution of mixins with quoted values
@@ -73,24 +94,14 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
73
94
  **Caution! You need to take care, no protection against infinite recursion **
74
95
 
75
96
  ### Special mixins
76
- - `%table` | `%table_name` | `%table_names`
77
- - `%column` | `%columns` | `%column_names`
97
+ - `%table` +String+, +ActiveRecord::Base+, Object responding to #to_sql, and +Array+ of these
98
+ - `%columns` +Array+ of +String+, +Hash+ keys: AS +Symbol+, +String+. fallback: 1) %casts keys, 2) %table.columns
99
+ - `%casts` +Hash+ keys: column name, values: Cast e.g. "text", "integer"
78
100
  - `%ident` | `%constraint` | `%constraints` quoting for database columns
79
101
  - `%raw` | `%sql` inserting raw SQL
80
- - `%value` | `%values` creates value section for e.g. insert
81
- - In the right order
82
- - Single value => (2)
83
- - +Array+ => (column, column, column) n.b. has to be the correct order
84
- - +Array+ of +Array+ => (...),(...),(...),...
85
- - if the columns option is given (or implicitely by setting table)
86
- - +Hash+ values are ordered according to the columns option, missing values are replaced by `DEFAULT`
87
- - +Array+ of +Hash+ multiple record insert
88
- - `%bind` is replaced with the current bind sequence.
89
- Without appended number the first %bind => $1, the second => $2 etc.
90
- - %bind\\d+ => $+Integer+ e.g. `%bind7` => $7
91
- - `%bind__text` => $1 and it is registered as text - this is used in prepared statements (TO BE IMPLEMENTED)
92
- - `%key_bind__text` => $1 and it is registered as text when using +Hash+ in the execute
93
- $1 will be mapped to the key's value in the +Hash+ TODO
102
+ - `%values` creates the value section for INSERT `INSERT INTO foo (a,b) %values`
103
+ - `%x_values` creates the value secion for FROM `SELECT column1, column2, column3 FROM %x_values`
104
+ - `%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)`
94
105
 
95
106
  All can be preceded by additional letters and underscore e.g. `%foo_bar_column`
96
107
 
@@ -98,10 +109,8 @@ All can be preceded by additional letters and underscore e.g. `%foo_bar_column`
98
109
  A database typecast is added to fields ending with double underscore and a valid db data type
99
110
  with optional array dimension
100
111
 
101
- - `%field__jsonb` => adds a `::JSONB` typecast to the field
102
- - `%number_to__text` => adds a `::TEXT` typecast to the field
103
- - `%array__text1` => adds a `::TEXT[]` (TO BE IMPLEMENTED)
104
- - `%array__text2` => adds a `::TEXT[][]` (TO BE IMPLEMENTED)
112
+ - `%field::jsonb` => treats the field as jsonb when casted
113
+ - `%array::text[]` => treats an array like a text array, default is JSONB
105
114
 
106
115
  ### Quoting
107
116
  - Any value of the standard mixins are quoted with these exceptions
@@ -110,22 +119,27 @@ with optional array dimension
110
119
  - When the value responds to :to_sql or is a +Arel::Nodes::SqlLiteral+ its added as raw SQL
111
120
  - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
112
121
 
113
- ### Special quoting columns
114
- - +String+ or +Symbol+ without a dot e.g. :firstname => "firstname"
115
- - +String+ or +Symbol+ containing a dot e.g. "users.firstname" or => "users"."firstname"
122
+ ### Special quoting for %columns
123
+
124
+ `QuoteSql.new("SELECT %columns FROM %table, other_table").quote(columns: ["a", "other_table.a", :a ], table: "my_table")`
125
+ => SELECT "a", "other_table"."a", "my_table"."a" from "my_table", "other_table"
126
+
127
+ - +String+ without a dot e.g. "firstname" => "firstname"
128
+ - +String+ containing a dot e.g. "users.firstname" or => "users"."firstname"
129
+ - +Symbol+ prepended with table from table: quote if present.
130
+ - +Proc+ is called in the current context
131
+ - +QuoteSql::Raw+ or +Arel::Nodes::SqlLiteral+ are injected as is
132
+ - Object responding to #to_sql is called and injected
116
133
  - +Array+
117
- - +String+ and +Symbols+ see above
118
134
  - +Hash+ see below
119
- - +Hash+ or within the +Array+
120
- - +Symbol+ value will become the column name e.g. {table: :column} => "table"."column"
121
- - +String+ value will become the expression, the key the AS {result: "SUM(*)"} => SUM(*) AS result
122
- - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
135
+ - other see above
136
+ - +Hash+
137
+ - keys become the "AS"
138
+ - values
139
+ - +Hash+, +Array+ casted as JSONB
140
+ - others see above
141
+
123
142
 
124
- ## Executing
125
- ### Getting the results
126
- ### Binds
127
- `v = {a: 1, b: "foo", c: true};QuoteSQL(%q{Select * From %x_json}, x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"}).result(v.to_json)`
128
- => Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean) => [{a: 1, b: "foo", c: true}]
129
143
 
130
144
  ## Shortcuts and functions
131
145
  - `QuoteSQL("select %abc", abc: 1)` == `QuoteSql.new("select %abc").quote(abc: 1)`
@@ -137,8 +151,8 @@ If you have pg_format installed you can get the resulting query inspected:
137
151
  `QuoteSql.new("select %abc").quote(abc: 1).dsql`
138
152
 
139
153
  # Test
140
- Minimal tests you can run by
141
- `QuoteSql.test.all`
154
+ Currently there are just minimal tests
155
+ run `QuoteSql.test`
142
156
  You can find them in /lib/quote_sql/test.rb
143
157
 
144
158
  ## Installing
@@ -151,8 +165,15 @@ Add this to config/initializers/quote_sql.rb
151
165
 
152
166
  ActiveSupport.on_load(:active_record) do
153
167
  require 'quote_sql'
154
- QuoteSql.db_connector = ActiveRecord::Base
168
+
169
+ # if you want to execute from Strings
170
+ # e.g. "select %a".quote_sql(a: 1).result
155
171
  String.include QuoteSql::Extension
172
+
173
+ # if you use Active Record
174
+ QuoteSql.db_connector = ActiveRecord::Base
175
+ # if you want to execute from a Model
176
+ # e.g. User.select("name, %a").quote_sql(a: 1).result
156
177
  ActiveRecord::Relation.include QuoteSql::Extension
157
178
  end
158
179
 
@@ -1,3 +1,4 @@
1
+ require 'niceql'
1
2
  class QuoteSql
2
3
  module Formater
3
4
  PG_FORMAT_BIN = `which pg_format`.chomp.presence
@@ -9,15 +10,14 @@ class QuoteSql
9
10
 
10
11
  def to_formatted_sql
11
12
  sql = respond_to?(:to_sql) ? to_sql : to_s
12
- Niceql::Prettifier.prettify_sql(sql)
13
+ Niceql::Prettifier.prettify_sql(sql.gsub(/(?<=[^%])%(?=\S)/, "%%"))
13
14
 
14
15
  # IO.popen(PG_FORMAT_BIN, "r+", err: "/dev/null") do |f|
15
16
  # f.write(sql)
16
17
  # f.close_write
17
18
  # f.read
18
19
  # end
19
- rescue
20
- sql
20
+
21
21
  end
22
22
 
23
23
  alias to_sqf to_formatted_sql
@@ -1,12 +1,12 @@
1
1
  class QuoteSql
2
2
  class Quoter
3
- def initialize(qsql, key, quotable)
3
+ def initialize(qsql, key, cast, quotable)
4
4
  @qsql = qsql
5
- @key, @quotable = key, quotable
5
+ @key, @cast, @quotable = key, cast, quotable
6
6
  @name = key.sub(/_[^_]+$/, '') if key["_"]
7
7
  end
8
8
 
9
- attr_reader :key, :quotable, :name
9
+ attr_reader :key, :quotable, :name, :cast
10
10
 
11
11
  def quotes
12
12
  @qsql.quotes
@@ -227,42 +227,65 @@ class QuoteSql
227
227
  end
228
228
  end
229
229
 
230
- def cast
231
- if m = key.to_s[CASTS]
232
- m[2..].sub(CASTS) { _1.tr("_", " ") }
233
- end
234
- end
235
-
236
- def json?
237
- !!key[/(^|_)(jsonb?)$/]
230
+ def json?(cast = self.cast)
231
+ cast.to_s[/jsonb?$/i]
238
232
  end
239
233
 
240
- private def _quote(item = @quotable, cast = self.cast)
241
- rv = QuoteSql.quote(item)
242
- if cast
243
- rv << "::#{cast.upcase}"
244
- rv << "[]" * rv.depth if rv[/^ARRAY/]
245
- end
246
- Raw.sql rv
234
+ private def _quote(item = @quotable)
235
+ Raw.sql QuoteSql.quote(item)
247
236
  end
248
237
 
249
238
  private def _quote_column_name(name)
250
239
  Raw.sql name.scan(/(?:^|")?([^."]+)/).map { QuoteSql.quote_column_name _1 }.join(".")
251
240
  end
252
241
 
253
- def quote(item = @quotable)
254
- case item.class.to_s
255
- when "Arel::Nodes::SqlLiteral", "QuoteSql::Raw"
256
- return Raw.sql(item)
257
- when "Array"
258
- return _quote(item.to_json) if json?
259
- _quote(item)
260
- when "Hash"
261
- _quote(item.to_json, :jsonb)
262
- else
263
- return Raw.sql item.to_sql if item.respond_to? :to_sql
264
- _quote(item)
242
+ private def _quote_array(items)
243
+ rv = items.map do |i|
244
+ if i.is_a?(Array)
245
+ _quote_array(i)
246
+ elsif self.cast[/jsonb?/i]
247
+ _quote(i.to_json)
248
+ else
249
+ quote(i)
250
+ end
265
251
  end
252
+ "[#{rv.join(",")}]"
253
+ end
254
+
255
+ def quote_hash(item)
256
+ item.compact! if item.delete(nil) == false
257
+ case self.cast
258
+ when /hstore/i
259
+ _quote(item.map { "#{_1}=>#{_2.nil? ? 'NULL' : _2}"}.join(","))
260
+ when NilClass,""
261
+ "#{_quote(item.to_json)}::JSONB"
262
+ when /jsonb?/i
263
+ _quote(item.to_json)
264
+ end
265
+ end
266
+
267
+ def quote(item = @quotable, cast = nil)
268
+ Raw.sql case item.class.to_s
269
+ when "Arel::Nodes::SqlLiteral", "QuoteSql::Raw"
270
+ item
271
+ when "Array"
272
+ if json? or self.cast.blank?
273
+ rv = _quote(item.to_json)
274
+ self.cast.present? ? rv : "#{rv}::JSONB"
275
+ else
276
+ "ARRAY#{_quote_array(item)}"
277
+ end
278
+ when "Hash"
279
+ quote_hash(item)
280
+ else
281
+ if item.respond_to? :to_sql
282
+ item.to_sql
283
+ elsif json?
284
+ _quote(item.to_json)
285
+ else
286
+ _quote(item)
287
+ end
288
+ end
266
289
  end
267
290
 
268
291
  def column_names(item = @quotable)
@@ -1,5 +1,6 @@
1
1
  class QuoteSql::Test
2
2
  private
3
+
3
4
  def test_columns
4
5
  expected <<~SQL
5
6
  SELECT x, "a", "b", "c", "d"
@@ -49,14 +50,14 @@ class QuoteSql::Test
49
50
  )
50
51
  end
51
52
 
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
53
+ # def test_binds
54
+ # expected <<~SQL
55
+ # SELECT $1, $2::UUID, $1 AS get_bind_1_again FROM "my_table"
56
+ # SQL
57
+ # QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table").quote(
58
+ # table: "my_table"
59
+ # )
60
+ # end
60
61
 
61
62
  def test_from_values_array
62
63
  expected <<~SQL
@@ -124,29 +125,59 @@ class QuoteSql::Test
124
125
  expected <<~SQL
125
126
  SELECT * FROM json_to_recordset('[{"a":1,"b":"foo"},{"a":"2"}]') as "x" ("a" int, "b" text)
126
127
  SQL
127
- "SELECT * FROM %x_json".quote_sql(x_casts: {a: "int", b: "text"}, x_json: [{ a: 1, b: 'foo'}, {a: '2', c: 'bar'}])
128
+ "SELECT * FROM %x_json".quote_sql(x_casts: { a: "int", b: "text" }, x_json: [{ a: 1, b: 'foo' }, { a: '2', c: 'bar' }])
128
129
  end
129
130
 
130
131
  def test_json_insert
131
132
  expected <<~SQL
132
- INSERT INTO users (name, color) SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "x"("name" text,"color" text)
133
+ INSERT INTO users (name, color) SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "x"("name" text,"color" text)
133
134
  SQL
134
- x_json = {"first_name"=>nil, "last_name"=>nil, "stripe_id"=>nil, "credits"=>nil, "avatar"=>nil, "name"=>"auge", "color"=>"#611333", "founder"=>nil, "language"=>nil, "country"=>nil, "data"=>{}, "created_at"=>"2020-11-19T09:30:18.670Z", "updated_at"=>"2020-11-19T09:40:00.063Z"}
135
- "INSERT INTO users (name, color) SELECT * from %x_json".quote_sql(x_casts: {name: "text", color: "text"}, x_json:)
135
+ x_json = { "first_name" => nil, "last_name" => nil, "stripe_id" => nil, "credits" => nil, "avatar" => nil, "name" => "auge", "color" => "#611333", "founder" => nil, "language" => nil, "country" => nil, "data" => {}, "created_at" => "2020-11-19T09:30:18.670Z", "updated_at" => "2020-11-19T09:40:00.063Z" }
136
+ "INSERT INTO users (name, color) SELECT * from %x_json".quote_sql(x_casts: { name: "text", color: "text" }, x_json:)
136
137
  end
137
138
 
138
139
  def test_from_json_bind
139
140
  expected <<~SQL
140
- Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
141
+ Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
141
142
  SQL
142
- QuoteSQL("Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
143
+ QuoteSQL("Select * From %x_json", x_json: 1, x_casts: { a: "int", b: "text", c: "boolean" })
143
144
  end
144
145
 
145
146
  def test_insert_json_bind
146
147
  expected <<~SQL
147
- INSERT INTO table ("a","b","c") Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
148
+ INSERT INTO table ("a","b","c") Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
149
+ SQL
150
+ QuoteSQL("INSERT INTO table (%x_columns) Select * From %x_json", x_json: 1, x_casts: { a: "int", b: "text", c: "boolean" })
151
+ end
152
+
153
+ def test_cast_values
154
+ expected <<~SQL
155
+ SELECT
156
+ 'abc'::TEXT,
157
+ '"abc"'::JSON,
158
+ '["cde",null,"fgh"]'::JSONB,
159
+ ARRAY['cde', NULL, 'fgh']::TEXT[],
160
+ ARRAY['"cde"', 'null', '"fgh"']::JSON[],
161
+ '{"foo":"bar","go":1,"strip_null":null}'::JSONB not_compact,
162
+ '{"foo":"bar","go":1}'::JSON compact,
163
+ 'foo=>bar,go=>1,strip_null=>NULL'::HSTORE,
164
+ ARRAY[[1,2,3],[1,2,3]]::INT[][]
165
+ SQL
166
+ array1 = array2 = array3 = ["cde", nil, "fgh"]
167
+ array4 = [[1,2,3], [1,2,3]]
168
+ hash = { foo: "bar", "go": 1, strip_null: nil }
169
+ QuoteSQL(<<~SQL, field1: 'abc', array1:, array2:, array3:, array4:, hash: ,not_compact: hash, compact: hash.merge(nil => false))
170
+ SELECT
171
+ %field1::TEXT,
172
+ %field1::JSON,
173
+ %array1,
174
+ %array2::TEXT[],
175
+ %array3::JSON[],
176
+ %not_compact not_compact,
177
+ %compact::JSON compact,
178
+ %hash::HSTORE,
179
+ %array4::INT[][]
148
180
  SQL
149
- QuoteSQL("INSERT INTO table (%x_columns) Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
150
181
  end
151
182
 
152
183
  # def test_q3
@@ -172,7 +203,6 @@ class QuoteSql::Test
172
203
  # )
173
204
  # end
174
205
 
175
-
176
206
  public
177
207
 
178
208
  def all
@@ -193,10 +223,12 @@ class QuoteSql::Test
193
223
  tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
194
224
  columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
195
225
  rv += [
196
- "QuoteSql.new(\"#{@test.original}\").quote(#{{**tables, **columns, **@test.quotes }.inspect}).to_sql", "🎯 #{expected}", "✅ #{sql}"]
226
+ "QuoteSql.new(\"#{@test.original}\").quote(#{{ **tables, **columns, **@test.quotes }.inspect}).to_sql", "🎯 #{expected}", "✅ #{sql}"]
197
227
  @success << rv if @success
198
228
  else
199
229
  rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
230
+ rv << sql.gsub(/\s+/, "")&.downcase&.strip
231
+ rv << expected&.gsub(/\s+/, "")&.downcase&.strip
200
232
  @fail << rv if @fail
201
233
  end
202
234
  rescue => exc
@@ -228,4 +260,111 @@ class QuoteSql::Test
228
260
  end
229
261
  end
230
262
 
231
- end
263
+ def datatype
264
+ errors = {}
265
+ success = []
266
+ spaces = ->(*) { " " * (rand(4) + 1) }
267
+
268
+ DATATYPES.each_line(chomp: true) do |line|
269
+
270
+ l = line.gsub(/\s+/, &spaces).gsub(/(?<=\()\d+|\d+(?=\))/) { "#{spaces.call}#{rand(10) + 1}#{spaces.call}" }.gsub(/\(/) { "#{spaces.call}(" }
271
+
272
+ m = "jgj hsgjhsgfjh ag %field::#{l} asldfalskjdfl".match(QuoteSql::CASTS)
273
+ if m.present? and l == m[1]
274
+ success << line
275
+ else
276
+ errors[line] = m&.to_a
277
+ end
278
+ line = line + "[]"*(rand(3) + 1)
279
+ m = "jgj hsgjhsgfjh ag %field::#{line} asldfalskjdfl".match(QuoteSql::CASTS)
280
+ if m.present? and line == m[1] + m[2]
281
+ success << line
282
+ else
283
+ errors[line] = m&.to_a
284
+ end
285
+ end
286
+ puts success.sort.inspect
287
+ ap errors
288
+ end
289
+
290
+ DATATYPES = <<-DATATYPES
291
+ bigint
292
+ int8
293
+ bigserial
294
+ serial8
295
+ bit
296
+ bit (1)
297
+ bit varying
298
+ varbit
299
+ bit varying (2)
300
+ varbit (2)
301
+ boolean
302
+ bool
303
+ box
304
+ bytea
305
+ character
306
+ char
307
+ character (1)
308
+ char (1)
309
+ character varying
310
+ varchar
311
+ character varying (1)
312
+ varchar (1)
313
+ cidr
314
+ circle
315
+ date
316
+ double precision
317
+ float8
318
+ inet
319
+ integer
320
+ int
321
+ int4
322
+ interval
323
+ interval (1)
324
+ json
325
+ jsonb
326
+ line
327
+ lseg
328
+ macaddr
329
+ macaddr8
330
+ money
331
+ numeric
332
+ numeric(10,3)
333
+ decimal
334
+ decimal(10,3)
335
+ path
336
+ pg_lsn
337
+ pg_snapshot
338
+ point
339
+ polygon
340
+ real
341
+ float4
342
+ smallint
343
+ int2
344
+ smallserial
345
+ serial
346
+ serial2
347
+ serial4
348
+ text
349
+ time
350
+ time(1)
351
+ time without time zone
352
+ time(1) without time zone
353
+ time with time zone
354
+ time(2) with time zone
355
+ timetz
356
+ timestamp
357
+ timestamp(1)
358
+ timestamp without time zone
359
+ timestamp(1) without time zone
360
+ timestamp with time zone
361
+ timestamp(1) with time zone
362
+ timestamptz
363
+ tsquery
364
+ tsvector
365
+ txid_snapshot
366
+ uuid
367
+ xml
368
+ DATATYPES
369
+
370
+ end
@@ -1,3 +1,3 @@
1
1
  class QuoteSql
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.7"
3
3
  end
data/lib/quote_sql.rb CHANGED
@@ -3,25 +3,30 @@ Dir.glob(__FILE__.sub(/\.rb$/, "/*.rb")).each { require(_1) unless _1[/(deprecat
3
3
  # Tool to build and run SQL queries easier
4
4
  class QuoteSql
5
5
 
6
+
6
7
  DATA_TYPES_RE = %w(
7
- (?:small|big)(?:int|serial)
8
- bit bool(?:ean)? box bytea cidr circle date
8
+ (?>character\\s+varying|bit\\s+varying|character|varbit|varchar|char|bit|interval)(?>\\s*\\(\\s*\\d+\\s*\\))?
9
+ (?>numeric|decimal)(?>\\s*\\(\\s*\\d+\\s*,\\s*\\d+\\s*\\))?
10
+ timestamptz timetz
11
+ time(?>stamp)?(?>\\s*\\(\\s*\\d+\\s*\\))?(?>\\s+with(?>out)?\\s+time\\s+zone)?
12
+ integer
13
+ (?>small|big)(?>int|serial)
14
+ bool(?>ean)? box bytea cidr circle date
9
15
  (?:date|int[48]|num|ts(?:tz)?)(?:multi)?range
10
16
  macaddr8?
11
- jsonb?
12
- ts(?:query|vector)
13
- float[48] (?:int|serial)[248]?
14
- double_precision inet
15
- integer line lseg money path pg_lsn
16
- pg_snapshot point polygon real text timestamptz timetz
17
- txid_snapshot uuid xml
18
- (bit_varying|varbit|character|char|character varying|varchar)(_\\(\\d+\\))?
19
- (numeric|decimal)(_\\(\d+_\d+\\))?
20
- interval(_(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND|YEAR_TO_MONTH|DAY_TO_HOUR|DAY_TO_MINUTE|DAY_TO_SECOND|HOUR_TO_MINUTE|HOUR_TO_SECOND|MINUTE_TO_SECOND))?(_\\(\d+\\))?
21
- time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
17
+ ts(?>query|vector)
18
+ float[48]
19
+ (?:int|serial)[248]?
20
+ double\\s+precision
21
+ jsonb json
22
+ inet
23
+ line lseg money path
24
+ pg_lsn pg_snapshot txid_snapshot
25
+ point polygon real text
26
+ uuid xml hstore
22
27
  ).join("|")
23
28
 
24
- CASTS = Regexp.new("__(#{DATA_TYPES_RE})$", "i")
29
+ CASTS = Regexp.new("::(#{DATA_TYPES_RE})((?:\\s*\\[\\s*\\d?\\s*\\])*)", "i")
25
30
 
26
31
  def self.conn
27
32
  raise ArgumentError, "You need to define a database connection function"
@@ -152,11 +157,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
152
157
  def key_matches
153
158
  @sql.scan(MIXIN_RE).map do |full, *key|
154
159
  key = key.compact[0]
155
- if m = key.match(/^(.+)#{CASTS}/i)
156
- _, key, cast = m.to_a
157
- end
158
160
  has_quote = @quotes.key?(key.to_sym) || key.match?(/(table|columns)$/)
159
- [full, key, cast, has_quote]
161
+ [full, key, has_quote]
160
162
  end
161
163
  end
162
164
 
@@ -166,27 +168,28 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
166
168
  loop do
167
169
  s = StringScanner.new(@sql)
168
170
  sql = ""
169
- key_matches.each do |key_match, key, cast, has_quote|
170
- s.scan_until(/(.*?)#{key_match}([a-z0-9_]*)/im)
171
- matched, pre, post = s.matched, s[1], s[2]
172
- if m = key.match(/^bind(\d+)?/im)
173
- if m[1].present?
174
- bind_num = m[1].to_i
175
- @binds[bind_num - 1] ||= cast
176
- raise "bind #{bind_num} already set to #{@binds[bind_num - 1]}" unless @binds[bind_num - 1] == cast
177
- else
178
- @binds << cast
179
- bind_num = @binds.length
180
- end
181
-
182
- matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
183
- elsif has_quote
184
- quoted = quoter(key)
171
+ key_matches.each do |key_match, key, has_quote|
172
+ s.scan_until(/(.*?)#{key_match}(#{CASTS}?)/im)
173
+ matched, pre, cast = s.matched, s[1], s[2]
174
+ # if m = key.match(/^bind(\d+)?/im)
175
+ # if m[1].present?
176
+ # bind_num = m[1].to_i
177
+ # @binds[bind_num - 1] ||= cast
178
+ # raise "bind #{bind_num} already set to #{@binds[bind_num - 1]}" unless @binds[bind_num - 1] == cast
179
+ # else
180
+ # @binds << cast
181
+ # bind_num = @binds.length
182
+ # end
183
+ #
184
+ # matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
185
+ # els
186
+ if has_quote
187
+ quoted = quoter(key, cast)
185
188
  unresolved.delete key
186
189
  if (i = quoted.scan MIXIN_RE).present?
187
190
  unresolved += i.map(&:last)
188
191
  end
189
- matched = "#{pre}#{quoted}#{post}"
192
+ matched = "#{pre}#{quoted}#{cast}"
190
193
  end
191
194
  rescue TypeError
192
195
  ensure
@@ -200,8 +203,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
200
203
  self
201
204
  end
202
205
 
203
- def quoter(key)
204
- quoter = @resolved[key.to_sym] = Quoter.new(self, key, @quotes[key.to_sym])
206
+ def quoter(key, cast)
207
+ quoter = @resolved[key.to_sym] = Quoter.new(self, key, cast, @quotes[key.to_sym])
205
208
  quoter.to_sql
206
209
  rescue TypeError => exc
207
210
  @resolved[key.to_sym] = exc
@@ -224,8 +227,11 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
224
227
 
225
228
  def self.test(which = :all)
226
229
  require __dir__ + "/quote_sql/test.rb"
227
- if which == :all
230
+ case which
231
+ when :all
228
232
  Test.new.all
233
+ when :datatype
234
+ Test.new.datatype
229
235
  else
230
236
  Test.new.run(which)
231
237
  end
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.5
4
+ version: 0.0.7
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-27 00:00:00.000000000 Z
11
+ date: 2024-02-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: niceql