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 +4 -4
- data/README.md +78 -45
- data/lib/quote_sql/quoter.rb +14 -6
- data/lib/quote_sql/test.rb +78 -62
- data/lib/quote_sql/version.rb +1 -1
- data/lib/quote_sql.rb +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e308b3aef586983a61155c9ba3be6c39c2397fa5fbae069ab45522e51681caa
|
4
|
+
data.tar.gz: 6a3945e1a4cfc16fed91c46216d1a10c5a69e85a6b3a0df10db45a9f162c07eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
8
|
-
|
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
|
-
|
9
|
+
Please have a look at the *unfinished* documentation below or run `QuoteSql.test` in a Ruby console
|
11
10
|
|
12
|
-
|
11
|
+
If you think QuoteSql is interesting but needs extension, let's chat!
|
13
12
|
|
14
|
-
If you
|
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 %
|
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
|
36
|
-
=> SELECT "a,b,jsonb_build_object('a', 1) FROM
|
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,
|
56
|
-
=> SELECT "a","b"."c",jsonb_build_object('
|
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
|
-
|
59
|
-
|
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`
|
75
|
-
- `%
|
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
|
-
- `%
|
79
|
-
|
80
|
-
|
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
|
-
|
113
|
-
|
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
|
-
-
|
118
|
-
|
119
|
-
-
|
120
|
-
-
|
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
|
-
|
133
|
-
`QuoteSql.test
|
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
|
-
|
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
|
|
data/lib/quote_sql/quoter.rb
CHANGED
@@ -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
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
147
|
-
|
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)
|
data/lib/quote_sql/test.rb
CHANGED
@@ -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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
data/lib/quote_sql/version.rb
CHANGED
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
|
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
|
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.
|
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
|
+
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-
|
11
|
+
date: 2024-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: niceql
|