querylet 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6eae3b4c9cb1beeea8edfbb41bab195ad133ac2c91d6393942f942d075860324
4
- data.tar.gz: 6632e474232ac27c6e64cd3d991b65e5331e61e97116280b9b8a9b00c1702a5f
3
+ metadata.gz: 89d1a03b5028328b79f0d575c2aeadec57ebf47f1d743fe5722b4c4a4b165fc6
4
+ data.tar.gz: 5db0889a5a8fc5a21aca04bb27f406522a22de46f91ef1fd95af5f0137983156
5
5
  SHA512:
6
- metadata.gz: c33031f332be7c159ce11f3fb4ec86cf8b606125ad3cf51cfd28c4aa7bb3600d2d5188b7bcb91664bc7b00d307cd258784b09fed852e5886cc8cd4e243ea22d6
7
- data.tar.gz: 1d5b1e8f732f7dd43dd110c442528a5af5b6d9939593d57bb5a9f5724e2e9d8fab9083265a885f2e57977f6612724babdc3a451aa0c786b0c4f7ab0edca19c30
6
+ metadata.gz: 296e9452984b0099681d8567ac291e1c71e9ee62cb706d96668927c0e2cab8062ac54f0c1186eed56bdf2e0a93ee463602a75928525fcb1d02eefe3089fae665
7
+ data.tar.gz: 3b1f422e8cabd1ee7dfbb977df2e3b77ed4d28a6b5bb93f0007cf20aff608737276a8504ca40239671dcd8b66d4e4f8bacaf2a8db92a62d746c200a5e7d7f687
data/README.md CHANGED
@@ -1,21 +1,118 @@
1
- ## Querylet
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
- #Variables
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
- #Partials
94
+ ### Partials
8
95
 
9
- {{>include 'link.to.path' param='{{testing}}'}}
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
- #Partial blocks
104
+ ```
12
105
  {{#array}}
13
106
  {{/array}}
107
+ ```
14
108
 
15
- # If Else
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
- Parslet parsers output deep nested hashes.
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/parser_helper.rb
159
+ bundle exec rspec spec/parser_spec.rb
65
160
  ```
66
161
 
67
162
  ## Transform
@@ -1,5 +1,10 @@
1
1
  module Querylet
2
- module Context
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 with_temporary_context(args = {})
16
- saved = args.keys.collect { |key| [key, get(key.to_s)] }.to_h
20
+ def get_partial name, dot_path, data
21
+ @querylet.get_partial name, dot_path, @data.merge(data)
22
+ end
17
23
 
18
- add_items(args)
19
- block_result = yield
20
- locals.merge!(saved)
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
- block_result
23
- end
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
 
@@ -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) { id.as(:variable) | string }
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 Qoutes 'content'
150
+ # String contained in Single Quotes 'content'
151
151
  rule(:string) { match("'") >> match("[^']").repeat(1).as(:string) >> match("'") }
152
152
  end
153
153
  end
@@ -1,4 +1,4 @@
1
- #require_relative 'context'
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
- if args
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(@querylet)
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
- context.add_item vals.first.to_s, vals.last._eval(context)
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
@@ -1,4 +1,4 @@
1
1
  module Querylet
2
- VERSION = "1.0.0"
2
+ VERSION = "1.2.0"
3
3
  end
4
4
 
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
- def set_context(ctx)
26
- @data = ctx
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(@data)
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
@@ -23,41 +23,40 @@ describe Querylet::Querylet do
23
23
 
24
24
  context 'helpers' do
25
25
  it 'include' do
26
- query = <<-SQL.chomp
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 = <<-SQL.chomp
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 = <<-SQL.chomp
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
- end # context 'evaluating'
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.0.0
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: 2020-12-05 00:00:00.000000000 Z
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.1.2
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