querylet 1.0.0 → 1.2.0
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 +105 -10
- data/lib/querylet/context.rb +19 -8
- data/lib/querylet/parser.rb +3 -3
- data/lib/querylet/template.rb +13 -7
- data/lib/querylet/tree.rb +6 -2
- data/lib/querylet/version.rb +1 -1
- data/lib/querylet.rb +4 -7
- data/spec/parser_spec.rb +19 -4
- data/spec/querylet_spec.rb +22 -23
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89d1a03b5028328b79f0d575c2aeadec57ebf47f1d743fe5722b4c4a4b165fc6
|
4
|
+
data.tar.gz: 5db0889a5a8fc5a21aca04bb27f406522a22de46f91ef1fd95af5f0137983156
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 296e9452984b0099681d8567ac291e1c71e9ee62cb706d96668927c0e2cab8062ac54f0c1186eed56bdf2e0a93ee463602a75928525fcb1d02eefe3089fae665
|
7
|
+
data.tar.gz: 3b1f422e8cabd1ee7dfbb977df2e3b77ed4d28a6b5bb93f0007cf20aff608737276a8504ca40239671dcd8b66d4e4f8bacaf2a8db92a62d746c200a5e7d7f687
|
data/README.md
CHANGED
@@ -1,21 +1,118 @@
|
|
1
|
-
|
1
|
+
# Querylet
|
2
|
+
|
3
|
+
Querylet is a templating language for SQL
|
4
|
+
and is designed to make writing raw SQL much easier.
|
5
|
+
|
6
|
+
```sql
|
7
|
+
SELECT
|
8
|
+
questions.id,
|
9
|
+
questions.uuid,
|
10
|
+
questions.question_html as question,
|
11
|
+
questions.target_type as kind,
|
12
|
+
{{#object}}
|
13
|
+
SELECT
|
14
|
+
case_study_questions.menu,
|
15
|
+
case_study_questions.questions_pool_size,
|
16
|
+
|
17
|
+
CASE {{str sub_question_type}}
|
18
|
+
WHEN 'MultiChoiceQuestion' THEN
|
19
|
+
{{> object 'questions.multiple_choice' id='{{sub_question_id}}' }} as question
|
20
|
+
WHEN 'MultiSelectQuestion' THEN
|
21
|
+
{{> object 'questions.multiple_select' id='{{sub_question_id}}' }} as question
|
22
|
+
END,
|
23
|
+
|
24
|
+
{{#array}}
|
25
|
+
SELECT
|
26
|
+
que.id,
|
27
|
+
que.uuid,
|
28
|
+
que.target_type as kind,
|
29
|
+
choices.comment,
|
30
|
+
choices.flagged,
|
31
|
+
choices.choice_value as answered,
|
32
|
+
multiple_select_questions.answers_count
|
33
|
+
FROM ts_admin_que.questions que
|
34
|
+
LEFT JOIN public.choices ON choices.question_id = que.id AND choices.attempt_id = {{id}}
|
35
|
+
LEFT JOIN ts_admin_que.multiple_select_questions ON multiple_select_questions.id = que.target_id AND que.target_type = 'MultipleSelectQuestion'
|
36
|
+
WHERE
|
37
|
+
que.id = ANY(case_study_questions.question_ids)
|
38
|
+
{{/array}} as questions
|
39
|
+
{{/object}} as case_study
|
40
|
+
FROM ts_admin_que.questions
|
41
|
+
LEFT JOIN ts_admin_que.case_study_questions ON case_study_questions.question_id = questions.id
|
42
|
+
WHERE
|
43
|
+
questions.id = {{id}}
|
44
|
+
```
|
45
|
+
|
46
|
+
## Variable Filters
|
47
|
+
|
48
|
+
### Int (Integer)
|
49
|
+
|
50
|
+
Cast variable to integer
|
51
|
+
|
52
|
+
```
|
53
|
+
{{int my_number}}
|
54
|
+
```
|
55
|
+
|
56
|
+
### Str (String)
|
57
|
+
|
58
|
+
Cast variable to string
|
2
59
|
|
3
|
-
|
60
|
+
```
|
61
|
+
{{str my_string}}
|
62
|
+
```
|
4
63
|
|
64
|
+
### Float
|
65
|
+
|
66
|
+
Cast variable to float
|
67
|
+
|
68
|
+
```
|
69
|
+
{{float my_number}}
|
70
|
+
```
|
71
|
+
|
72
|
+
### Arr (Array)
|
73
|
+
|
74
|
+
Cast to array (not a postgres array)
|
75
|
+
|
76
|
+
```
|
77
|
+
{{arr my_array}}
|
78
|
+
```
|
79
|
+
|
80
|
+
### Wild (Wildcard)
|
81
|
+
|
82
|
+
Wildcard front and back of string
|
83
|
+
|
84
|
+
```
|
85
|
+
{{wild my_string}}
|
86
|
+
```
|
87
|
+
|
88
|
+
### Variables
|
89
|
+
|
90
|
+
```
|
5
91
|
{{my_variable}}
|
92
|
+
```
|
6
93
|
|
7
|
-
|
94
|
+
### Partials
|
8
95
|
|
9
|
-
|
96
|
+
```
|
97
|
+
{{> include 'link.to.path' name='andrew' }}
|
98
|
+
{{> include 'link.to.path' name={{my_name}} }}
|
99
|
+
{{> include 'link.to.path' name={{str my_name}} }}
|
100
|
+
```
|
101
|
+
|
102
|
+
### Partial blocks
|
10
103
|
|
11
|
-
|
104
|
+
```
|
12
105
|
{{#array}}
|
13
106
|
{{/array}}
|
107
|
+
```
|
14
108
|
|
15
|
-
|
109
|
+
### If Else
|
110
|
+
|
111
|
+
```
|
16
112
|
{{#if variable}}
|
17
113
|
{{else}}
|
18
114
|
{{/end}}
|
115
|
+
```
|
19
116
|
|
20
117
|
## Parslet
|
21
118
|
|
@@ -56,12 +153,10 @@ parser.parse_with_debug(input)
|
|
56
153
|
|
57
154
|
## Parser
|
58
155
|
|
59
|
-
|
60
|
-
|
61
|
-
To see all the define rules check out `parser_helper.rb`
|
156
|
+
To see all the define rules check out `parser_spec.rb`
|
62
157
|
|
63
158
|
```
|
64
|
-
rspec spec/
|
159
|
+
bundle exec rspec spec/parser_spec.rb
|
65
160
|
```
|
66
161
|
|
67
162
|
## Transform
|
data/lib/querylet/context.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
module Querylet
|
2
|
-
|
2
|
+
class Context
|
3
|
+
def initialize(querylet, data)
|
4
|
+
@querylet = querylet
|
5
|
+
@data = data
|
6
|
+
end
|
7
|
+
|
3
8
|
def get(value)
|
4
9
|
@data.merge(locals)[value.to_sym]
|
5
10
|
end
|
@@ -12,15 +17,21 @@ module Querylet
|
|
12
17
|
hash.map { |k, v| add_item(k, v) }
|
13
18
|
end
|
14
19
|
|
15
|
-
def
|
16
|
-
|
20
|
+
def get_partial name, dot_path, data
|
21
|
+
@querylet.get_partial name, dot_path, @data.merge(data)
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
|
20
|
-
|
24
|
+
# This never appears to be used
|
25
|
+
# commented out unless I find a use for it.
|
26
|
+
#def with_temporary_context(args = {})
|
27
|
+
# saved = args.keys.collect { |key| [key, get(key.to_s)] }.to_h
|
21
28
|
|
22
|
-
|
23
|
-
|
29
|
+
# add_items(args)
|
30
|
+
# block_result = yield
|
31
|
+
# locals.merge!(saved)
|
32
|
+
|
33
|
+
# block_result
|
34
|
+
#end
|
24
35
|
|
25
36
|
private
|
26
37
|
|
data/lib/querylet/parser.rb
CHANGED
@@ -96,7 +96,7 @@ module Querylet
|
|
96
96
|
rule(:variable) { docurly >> space? >> id.as(:variable) >> space? >> dccurly}
|
97
97
|
|
98
98
|
# Can either be a variable or a string:
|
99
|
-
rule(:parameter)
|
99
|
+
rule(:parameter) { id.as(:variable) | string }
|
100
100
|
|
101
101
|
# used in includes eg. hello='world'
|
102
102
|
rule(:parameter_kv) {
|
@@ -104,7 +104,7 @@ module Querylet
|
|
104
104
|
space? >>
|
105
105
|
eq >>
|
106
106
|
space? >>
|
107
|
-
string.as(:value) >>
|
107
|
+
(filter | variable | string).as(:value) >>
|
108
108
|
space?
|
109
109
|
}
|
110
110
|
|
@@ -147,7 +147,7 @@ module Querylet
|
|
147
147
|
rule(:nocurly) { match('[^{}]') }
|
148
148
|
rule(:eof) { any.absent? }
|
149
149
|
|
150
|
-
# String contained in Single
|
150
|
+
# String contained in Single Quotes 'content'
|
151
151
|
rule(:string) { match("'") >> match("[^']").repeat(1).as(:string) >> match("'") }
|
152
152
|
end
|
153
153
|
end
|
data/lib/querylet/template.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative 'context'
|
2
2
|
|
3
3
|
module Querylet
|
4
4
|
class Template
|
@@ -7,13 +7,19 @@ module Querylet
|
|
7
7
|
@ast = ast
|
8
8
|
end
|
9
9
|
|
10
|
+
# In Ruby, the call method is used to invoke a block, proc, or lambda,
|
11
|
+
# which are all different types of callable objects in Ruby.
|
12
|
+
#
|
13
|
+
# This is how querylet will get called in a developers code.
|
14
|
+
#
|
15
|
+
# querylet = Querylet::Querylet.new path: 'path/to/sql'
|
16
|
+
# querylet.compile(template).call(data)
|
17
|
+
# @args is the initial values passed to the template
|
10
18
|
def call(args = nil)
|
11
|
-
|
12
|
-
@querylet.set_context(args)
|
13
|
-
end
|
19
|
+
ctx = Context.new(@querylet, args)
|
14
20
|
|
15
21
|
# AST should return a Querylet::Tree and call its eval method
|
16
|
-
@ast.eval(
|
22
|
+
@ast.eval(ctx)
|
17
23
|
end
|
18
|
-
end
|
19
|
-
end
|
24
|
+
end # Template
|
25
|
+
end # Querylet
|
data/lib/querylet/tree.rb
CHANGED
@@ -36,10 +36,12 @@ module Querylet
|
|
36
36
|
|
37
37
|
class Partial < TreeItem.new(:partial, :path, :parameters)
|
38
38
|
def _eval(context)
|
39
|
+
data = {}
|
39
40
|
[parameters].flatten.map(&:values).map do |vals|
|
40
|
-
|
41
|
+
# we have to set it as sym so it overrides correctly
|
42
|
+
data[vals.first.to_sym] = vals.last._eval(context)
|
41
43
|
end
|
42
|
-
content = context.get_partial(partial.to_s, path)
|
44
|
+
content = context.get_partial(partial.to_s, path, data)
|
43
45
|
if partial == 'array'
|
44
46
|
<<-HEREDOC.chomp
|
45
47
|
(SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
|
@@ -131,6 +133,8 @@ module Querylet
|
|
131
133
|
else
|
132
134
|
raise "expected input for: #{parameter.item} to be String, Integer or Array"
|
133
135
|
end
|
136
|
+
else
|
137
|
+
raise "unexpected filter name: #{filter}"
|
134
138
|
end
|
135
139
|
end
|
136
140
|
end
|
data/lib/querylet/version.rb
CHANGED
data/lib/querylet.rb
CHANGED
@@ -6,7 +6,6 @@ require_relative 'querylet/context'
|
|
6
6
|
|
7
7
|
module Querylet
|
8
8
|
class Querylet
|
9
|
-
include Context
|
10
9
|
def initialize(path:)
|
11
10
|
@sql_path = path
|
12
11
|
end
|
@@ -19,17 +18,15 @@ module Querylet
|
|
19
18
|
#puts deep_nested_hash
|
20
19
|
abstract_syntax_tree = transform.apply deep_nested_hash
|
21
20
|
#puts abstract_syntax_tree
|
22
|
-
Template.new self, abstract_syntax_tree
|
23
|
-
end
|
24
21
|
|
25
|
-
|
26
|
-
|
22
|
+
# Querylet::Template
|
23
|
+
Template.new self, abstract_syntax_tree
|
27
24
|
end
|
28
25
|
|
29
|
-
def get_partial name, dot_path
|
26
|
+
def get_partial name, dot_path, data={}
|
30
27
|
path = @sql_path + '/' + dot_path.to_s.split('.').join('/') + '.sql'
|
31
28
|
template = File.read(path).to_s.chomp
|
32
|
-
self.compile(template).call(
|
29
|
+
self.compile(template).call(data)
|
33
30
|
end
|
34
31
|
end # class Querylet
|
35
32
|
end # module Querylet
|
data/spec/parser_spec.rb
CHANGED
@@ -10,10 +10,9 @@ RSpec.describe Querylet::Parser do
|
|
10
10
|
end
|
11
11
|
|
12
12
|
it 'string' do
|
13
|
-
(SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
|
14
|
-
|
15
13
|
expect(parser.parse_with_debug("(SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (")).to eq({items:[{content: "(SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM ("}]})
|
16
14
|
end
|
15
|
+
|
17
16
|
it 'variable' do
|
18
17
|
expect(parser.parse('{{worf}}')).to eq({items:[{variable: 'worf'}]})
|
19
18
|
expect(parser.parse('{{ worf}}')).to eq({items:[{variable: 'worf'}]})
|
@@ -52,8 +51,24 @@ RSpec.describe Querylet::Parser do
|
|
52
51
|
partial: 'include',
|
53
52
|
path: 'path.to.template',
|
54
53
|
parameters: [
|
55
|
-
{key: 'hello', value: {string: 'world'}},
|
56
|
-
{key: 'star', value: {string: 'trek'}}
|
54
|
+
{key: 'hello', value: { string: 'world'}},
|
55
|
+
{key: 'star' , value: { string: 'trek'}}
|
56
|
+
]
|
57
|
+
}]})
|
58
|
+
|
59
|
+
expect(parser.parse("{{> include 'path.to.template' hello={{world}} }}")).to eq({items:[{
|
60
|
+
partial: 'include',
|
61
|
+
path: 'path.to.template',
|
62
|
+
parameters: [
|
63
|
+
{key: 'hello', value: { variable: 'world'}}
|
64
|
+
]
|
65
|
+
}]})
|
66
|
+
|
67
|
+
expect(parser.parse("{{> include 'path.to.template' hello={{int world}} }}")).to eq({items:[{
|
68
|
+
partial: 'include',
|
69
|
+
path: 'path.to.template',
|
70
|
+
parameters: [
|
71
|
+
{key: 'hello', value: {filter: 'int', parameter: {variable: 'world'} }}
|
57
72
|
]
|
58
73
|
}]})
|
59
74
|
end
|
data/spec/querylet_spec.rb
CHANGED
@@ -23,41 +23,40 @@ describe Querylet::Querylet do
|
|
23
23
|
|
24
24
|
context 'helpers' do
|
25
25
|
it 'include' do
|
26
|
-
query =
|
27
|
-
(SELECT
|
28
|
-
users.email
|
29
|
-
FROM users
|
30
|
-
WHERE
|
31
|
-
users.id = 1) as email
|
32
|
-
SQL
|
26
|
+
query = "(SELECT users.email FROM users WHERE users.id = 1) as email"
|
33
27
|
expect(evaluate("({{> include 'examples.include' }}) as email")).to eq(query)
|
34
28
|
end
|
35
29
|
|
36
30
|
it 'include with variables' do
|
37
|
-
query =
|
38
|
-
(SELECT
|
39
|
-
users.email
|
40
|
-
FROM users
|
41
|
-
WHERE
|
42
|
-
users.id = 100) as email
|
43
|
-
SQL
|
31
|
+
query = "(SELECT users.email FROM users WHERE users.id = 100) as email"
|
44
32
|
template = "({{> include 'examples.include_with_vars' }}) as email"
|
45
33
|
expect(evaluate(template, {id: 100})).to eq(query)
|
46
34
|
end
|
47
35
|
|
48
36
|
it 'include with parameters' do
|
49
|
-
query =
|
50
|
-
(SELECT
|
51
|
-
users.email,
|
52
|
-
'andrew' as name
|
53
|
-
FROM users
|
54
|
-
WHERE
|
55
|
-
users.id = 200) as email
|
56
|
-
SQL
|
37
|
+
query = "(SELECT users.email, 'andrew' as name FROM users WHERE users.id = 200) as email"
|
57
38
|
template = "({{> include 'examples.include_with_params' id='200' name='andrew' }}) as email"
|
58
39
|
expect(evaluate(template, {id: 100})).to eq(query)
|
59
40
|
end
|
60
41
|
|
42
|
+
it 'include with parameters variable' do
|
43
|
+
query = "(SELECT users.email FROM users WHERE users.id = 300) as email"
|
44
|
+
template = "({{> include 'examples.include_with_params_vars' id={{user_id}} name='andrew' }}) as email"
|
45
|
+
expect(evaluate(template, {user_id: 300})).to eq(query)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'include with parameters filter' do
|
49
|
+
query = "(SELECT users.email, 'Andrew' as name FROM users WHERE users.id = 100) as email"
|
50
|
+
template = "({{> include 'examples.include_with_params_filter' name={{str my_name}} }}) as email"
|
51
|
+
expect(evaluate(template, {my_name: "Andrew", id: 100})).to eq(query)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'variable overrides' do
|
55
|
+
query = "(SELECT questions.id, (SELECT que.id FROM questions que WHERE que.id = 555) as sub FROM questions WHERE questions.id = 100"
|
56
|
+
template = "(SELECT questions.id, ({{> include 'examples.vars_overrides_include' xid={{sub_id}} }}) as sub FROM questions WHERE questions.id = {{xid}}"
|
57
|
+
expect(evaluate(template, {sub_id: 555, xid: 100})).to eq(query)
|
58
|
+
end
|
59
|
+
|
61
60
|
it 'object' do
|
62
61
|
query = <<-SQL.chomp
|
63
62
|
(SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM (
|
@@ -152,5 +151,5 @@ SQL
|
|
152
151
|
end
|
153
152
|
|
154
153
|
end # context 'helpers'
|
155
|
-
|
154
|
+
end # context 'evaluating'
|
156
155
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: querylet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ExamPro
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: parslet
|
@@ -74,7 +74,7 @@ homepage: http://exampro.co
|
|
74
74
|
licenses:
|
75
75
|
- MIT
|
76
76
|
metadata: {}
|
77
|
-
post_install_message:
|
77
|
+
post_install_message:
|
78
78
|
rdoc_options: []
|
79
79
|
require_paths:
|
80
80
|
- lib
|
@@ -89,11 +89,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
89
|
- !ruby/object:Gem::Version
|
90
90
|
version: '0'
|
91
91
|
requirements: []
|
92
|
-
rubygems_version: 3.
|
93
|
-
signing_key:
|
92
|
+
rubygems_version: 3.4.10
|
93
|
+
signing_key:
|
94
94
|
specification_version: 4
|
95
95
|
summary: Querylet
|
96
96
|
test_files:
|
97
|
-
- spec/spec_helper.rb
|
98
97
|
- spec/parser_spec.rb
|
99
98
|
- spec/querylet_spec.rb
|
99
|
+
- spec/spec_helper.rb
|