metasql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4d640f39caabdf0c37f23db612fe303cd6c4f18b4350c925cc16e887d0b7129b
4
+ data.tar.gz: 3dcd34230c397ea8ca9cd5c50815e9919a93a10be4ec1f5fcf12d255f0ef9403
5
+ SHA512:
6
+ metadata.gz: 3d9d0dac4349bcda86fc4d323f31ce4162b5efd4758a48e2379e419cfe0fc994fc19f7efd365ac100285835229a4ac64d9493ba86ea4d20fa54e2b8d03867b68
7
+ data.tar.gz: 6d2873e7e28b9e602250db89b58b3f43d9ce44575b18d6e58c1ed3599768fee6cab30ca58907489cca4b78d17d5b66e280dad92b54f6b7ffb23b609cfa531ac0
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby:
15
+ - '3.0'
16
+ - '2.7'
17
+ - '2.6'
18
+ - '2.5'
19
+ steps:
20
+ - uses: actions/checkout@v2
21
+ - uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby }}
24
+ bundler-cache: true
25
+ - name: rubocop
26
+ run: bundle exec rubocop
27
+ - name: RSpec
28
+ run: bundle exec rake spec
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,74 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ Exclude:
4
+ - 'vendor/**/*'
5
+ - 'spec/fixtures/**/*'
6
+ - 'tmp/**/*'
7
+ - '.git/**/*'
8
+ - 'bin/*'
9
+ TargetRubyVersion: 2.5
10
+ SuggestExtensions: false
11
+
12
+ Style/FrozenStringLiteralComment:
13
+ Enabled: false
14
+
15
+ Layout/EndOfLine:
16
+ EnforcedStyle: lf
17
+
18
+ Layout/ClassStructure:
19
+ Enabled: true
20
+ Categories:
21
+ module_inclusion:
22
+ - include
23
+ - prepend
24
+ - extend
25
+ ExpectedOrder:
26
+ - module_inclusion
27
+ - constants
28
+ - public_class_methods
29
+ - initializer
30
+ - instance_methods
31
+ - protected_methods
32
+ - private_methods
33
+
34
+ Lint/AmbiguousBlockAssociation:
35
+ Exclude:
36
+ - 'spec/**/*.rb'
37
+
38
+ Layout/HashAlignment:
39
+ EnforcedHashRocketStyle:
40
+ - key
41
+ - table
42
+ EnforcedColonStyle:
43
+ - key
44
+ - table
45
+
46
+ Layout/LineLength:
47
+ Max: 100
48
+ IgnoredPatterns:
49
+ - !ruby/regexp /\A +(it|describe|context|shared_examples|include_examples|it_behaves_like) ["']/
50
+ - !ruby/regexp /\A +(expect)[()]/
51
+
52
+ Lint/InterpolationCheck:
53
+ Exclude:
54
+ - 'spec/**/*.rb'
55
+
56
+ Lint/BooleanSymbol:
57
+ Enabled: false
58
+
59
+ Metrics/BlockLength:
60
+ Exclude:
61
+ - 'Rakefile'
62
+ - '**/*.rake'
63
+ - 'spec/**/*.rb'
64
+ - '**/*.gemspec'
65
+
66
+ Metrics/ModuleLength:
67
+ Exclude:
68
+ - 'spec/**/*.rb'
69
+
70
+ Metrics/MethodLength:
71
+ Max: 30
72
+
73
+ Metrics/AbcSize:
74
+ Max: 20
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in metasql.gemspec
4
+ gemspec
5
+
6
+ gem 'pry'
7
+ gem 'rake', '~> 12.0'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rubocop', '~> 1.12'
data/Gemfile.lock ADDED
@@ -0,0 +1,61 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ metasql (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ coderay (1.1.3)
11
+ diff-lcs (1.4.4)
12
+ method_source (1.0.0)
13
+ parallel (1.20.1)
14
+ parser (3.0.0.0)
15
+ ast (~> 2.4.1)
16
+ pry (0.14.0)
17
+ coderay (~> 1.1)
18
+ method_source (~> 1.0)
19
+ rainbow (3.0.0)
20
+ rake (12.3.3)
21
+ regexp_parser (2.1.1)
22
+ rexml (3.2.4)
23
+ rspec (3.10.0)
24
+ rspec-core (~> 3.10.0)
25
+ rspec-expectations (~> 3.10.0)
26
+ rspec-mocks (~> 3.10.0)
27
+ rspec-core (3.10.1)
28
+ rspec-support (~> 3.10.0)
29
+ rspec-expectations (3.10.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.10.0)
32
+ rspec-mocks (3.10.2)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.10.0)
35
+ rspec-support (3.10.2)
36
+ rubocop (1.12.0)
37
+ parallel (~> 1.10)
38
+ parser (>= 3.0.0.0)
39
+ rainbow (>= 2.2.2, < 4.0)
40
+ regexp_parser (>= 1.8, < 3.0)
41
+ rexml
42
+ rubocop-ast (>= 1.2.0, < 2.0)
43
+ ruby-progressbar (~> 1.7)
44
+ unicode-display_width (>= 1.4.0, < 3.0)
45
+ rubocop-ast (1.4.1)
46
+ parser (>= 2.7.1.5)
47
+ ruby-progressbar (1.11.0)
48
+ unicode-display_width (2.0.0)
49
+
50
+ PLATFORMS
51
+ ruby
52
+
53
+ DEPENDENCIES
54
+ metasql!
55
+ pry
56
+ rake (~> 12.0)
57
+ rspec (~> 3.0)
58
+ rubocop (~> 1.12)
59
+
60
+ BUNDLED WITH
61
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Nobuo Takizawa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # Metasql
2
+
3
+ ![Gem](https://img.shields.io/gem/v/metasql?style=flat-square)
4
+ ![GitHub](https://img.shields.io/github/license/nobuyo/metasql?style=flat-square)
5
+
6
+ Metasql is Metabase flavored query preprocessor.
7
+ Provides parameter substitution, optional clause deletion, etc.
8
+
9
+ # Getting Started
10
+
11
+ ```ruby
12
+ require 'metasql'
13
+
14
+ sql = <<~SQL
15
+ SELECT
16
+ *
17
+ FROM
18
+ items
19
+ WHERE
20
+ foo = TRUE
21
+ AND bar = {{ bar }}
22
+ [[
23
+ AND
24
+ CASE WHEN {{ baz }} < 10
25
+ THEN baz = {{ baz }}
26
+ ELSE TRUE
27
+ ]]
28
+ SQL
29
+
30
+ query = Metasql::Parser.parse(sql)
31
+
32
+ parameters = {
33
+ bar: 'hi',
34
+ baz: 10
35
+ }
36
+
37
+ # with parameter
38
+ print query.with(parameters).deparse
39
+ # => SELECT
40
+ # *
41
+ # FROM
42
+ # items
43
+ # WHERE
44
+ # foo = TRUE
45
+ # AND bar = 'hi'
46
+ #
47
+ # AND
48
+ # CASE WHEN 10 < 10
49
+ # THEN baz = 10
50
+ # ELSE TRUE
51
+
52
+ # without parameter
53
+ print query.deparse
54
+ # => Metasql::ParameterMissing: Required parameters missing: bar
55
+
56
+ # partially supply parameter
57
+ print query.with({ bar: 'hi' }).deparse
58
+ # => SELECT
59
+ # *
60
+ # FROM
61
+ # items
62
+ # WHERE
63
+ # foo = TRUE
64
+ # AND bar = 'hi'
65
+ ```
66
+
67
+ ## Contributing
68
+
69
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nobuyo/metasql.
70
+
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'metasql'
5
+ require 'pry'
6
+
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/lib/metasql.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'metasql/version'
2
+
3
+ require 'metasql/parameter'
4
+ require 'metasql/query'
5
+ require 'metasql/parser'
6
+ require 'metasql/errors'
@@ -0,0 +1,27 @@
1
+ module Metasql
2
+ class BaseError < StandardError; end
3
+
4
+ # InvalidQueryError is caused by Metabase query syntax error.
5
+ class InvalidQueryError < BaseError
6
+ attr_reader :message
7
+
8
+ def initialize(error_message)
9
+ @message = error_message
10
+ super
11
+ end
12
+ end
13
+
14
+ # ParameterMissing causes when required param is not supplied.
15
+ class ParameterMissing < BaseError
16
+ attr_reader :key
17
+
18
+ def initialize(key)
19
+ @key = key
20
+ super
21
+ end
22
+
23
+ def message
24
+ "Required parameters missing: #{key}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Metasql
2
+ # Param represents required parameter in Metabase query.
3
+ class Param
4
+ attr_accessor :name
5
+
6
+ def initialize(name)
7
+ @name = name.strip.to_sym
8
+ end
9
+ end
10
+
11
+ # Optional represents optional clause in Metabase query.
12
+ class Optional
13
+ attr_accessor :query
14
+
15
+ def initialize(query)
16
+ @query = query
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,110 @@
1
+ module Metasql
2
+ # Parser for Metabase flavored query.
3
+ class Parser
4
+ TOKENS = [
5
+ { token: :optional_begin, pattern: '[[' },
6
+ { token: :optional_end, pattern: ']]' },
7
+ { token: :param_begin, pattern: /(.*?)({{(?!{))(.*)/m },
8
+ { token: :param_end, pattern: '}}' }
9
+ ].map(&:freeze).freeze
10
+
11
+ # Generate new Metasql::Query from supplied string.
12
+ def self.parse(query)
13
+ parser = new(query: query)
14
+ tokenized = parser.tokenize
15
+ parsed = parser.parse_tokens(tokens: tokenized)
16
+
17
+ Query.new(parsed: parsed)
18
+ end
19
+
20
+ attr_reader :query
21
+
22
+ def initialize(query:)
23
+ @query = query
24
+ end
25
+
26
+ def tokenize
27
+ TOKENS.inject([query]) do |strs, token_definition|
28
+ strs.map do |s|
29
+ next [s] unless s.is_a?(String)
30
+
31
+ acc = []
32
+ target = s
33
+ loop do
34
+ break acc if target.nil?
35
+
36
+ result = split_on_token(target: target, token: token_definition)
37
+ break acc << target unless result.compact.size == 3
38
+
39
+ acc.concat(result[0..1])
40
+ target = result[2]
41
+ end
42
+ end.flatten
43
+ end
44
+ end
45
+
46
+ def parse_tokens(tokens:, depth: 0)
47
+ acc = []
48
+ remains = tokens
49
+
50
+ loop do
51
+ token, *remains = remains
52
+
53
+ if token.nil?
54
+ if depth.positive?
55
+ raise Metasql::InvalidQueryError,
56
+ "Invalid query: found '[[' or '{{' with no matching ']]' or '}}'"
57
+ end
58
+
59
+ break acc
60
+ end
61
+
62
+ if %i[optional_begin param_begin].include?(token)
63
+ parsed, remains = parse_tokens(tokens: remains, depth: depth + 1)
64
+ acc << send(token.to_s.split('_').first, parsed)
65
+
66
+ next [acc, remains]
67
+ end
68
+
69
+ if %i[optional_end param_end].include?(token)
70
+ acc << { optional_end: ']]', param_end: '}}' }[token] unless depth.positive?
71
+
72
+ break [acc, remains]
73
+ end
74
+
75
+ acc << token
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def param(args)
82
+ name, *other = args
83
+ if other.size > 1 || !name.is_a?(String)
84
+ raise Metasql::InvalidQueryError, "Invalid '{{...}}' clause: expected a param name"
85
+ end
86
+
87
+ Param.new(name)
88
+ end
89
+
90
+ def optional(parsed)
91
+ if parsed.none? { |t| t.is_a?(Param) }
92
+ raise Metasql::InvalidQueryError,
93
+ "'[[...]]' clauses must contain at least one '{{...}}' clause."
94
+ end
95
+
96
+ Optional.new(parsed)
97
+ end
98
+
99
+ def split_on_token(target:, token:)
100
+ case token[:pattern]
101
+ when String
102
+ before, after = target.split(token[:pattern], 2)
103
+ when Regexp
104
+ before, _, after = token[:pattern].match(target)&.captures
105
+ end
106
+
107
+ [before, token[:token], after]
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,60 @@
1
+ module Metasql
2
+ # Query ...
3
+ class Query
4
+ attr_accessor :parsed, :mapping
5
+
6
+ def initialize(parsed:, mapping: {})
7
+ @parsed = parsed
8
+ @mapping = mapping.transform_keys(&:to_sym)
9
+ end
10
+
11
+ def with(mapping)
12
+ dup.tap { |q| q.mapping = mapping }
13
+ end
14
+
15
+ # Rebuild query from parsed tokens.
16
+ # For supplying parameter value, call `with()` before this method.
17
+ def deparse
18
+ parsed.map do |token|
19
+ case token
20
+ when Metasql::Optional
21
+ substitute_optional(query_array: token.query)
22
+ when Metasql::Param
23
+ substitute_param(param: token)
24
+ else
25
+ token
26
+ end
27
+ end.join
28
+ end
29
+
30
+ private
31
+
32
+ def substitute_optional(query_array:)
33
+ if query_array.select { |t| t.is_a?(Metasql::Param) }.map(&:name).none? { |n| mapping[n] }
34
+ return ''
35
+ end
36
+
37
+ query_array.map do |m|
38
+ if m.is_a?(Metasql::Param)
39
+ quote_string(mapping[m.name])
40
+ else
41
+ m
42
+ end
43
+ end.join
44
+ end
45
+
46
+ def substitute_param(param:)
47
+ raise Metasql::ParameterMissing, param.name unless mapping[param.name]
48
+
49
+ quote_string(mapping[param.name])
50
+ end
51
+
52
+ def quote_string(value)
53
+ if value.is_a?(Numeric)
54
+ value
55
+ else
56
+ "'#{value}'"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Metasql
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/metasql.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ require_relative 'lib/metasql/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'metasql'
5
+ spec.version = Metasql::VERSION
6
+ spec.authors = ['Nobuo Takizawa']
7
+ spec.email = ['longzechansheng@gmail.com']
8
+
9
+ spec.summary = 'Resolve parameters of Metabase flavored query.'
10
+ spec.homepage = 'https://github.com/nobuyo/metasql'
11
+
12
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
13
+
14
+ spec.metadata['homepage_uri'] = spec.homepage
15
+ spec.metadata['source_code_uri'] = 'https://github.com/nobuyo/metasql'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metasql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nobuo Takizawa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-03-30 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - longzechansheng@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".github/workflows/ci.yml"
21
+ - ".gitignore"
22
+ - ".rspec"
23
+ - ".rubocop.yml"
24
+ - Gemfile
25
+ - Gemfile.lock
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - bin/console
30
+ - bin/setup
31
+ - lib/metasql.rb
32
+ - lib/metasql/errors.rb
33
+ - lib/metasql/parameter.rb
34
+ - lib/metasql/parser.rb
35
+ - lib/metasql/query.rb
36
+ - lib/metasql/version.rb
37
+ - metasql.gemspec
38
+ homepage: https://github.com/nobuyo/metasql
39
+ licenses: []
40
+ metadata:
41
+ homepage_uri: https://github.com/nobuyo/metasql
42
+ source_code_uri: https://github.com/nobuyo/metasql
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.5.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.1.4
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Resolve parameters of Metabase flavored query.
62
+ test_files: []