Skip to content

Commit a7c4872

Browse files
authored
Merge pull request #7 from sdogruyol/glob_reader
Add glob support to the loader.
2 parents 2e51651 + 9321e61 commit a7c4872

File tree

10 files changed

+115
-14
lines changed

10 files changed

+115
-14
lines changed

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Require Tren and load your SQL file. It's going to create a first class method t
3030
require "tren"
3131
3232
Tren.load("/path/to/your/file.sql")
33+
34+
# Or you can load multiple files at once:
35+
Tren.load("./db/**/*.sql")
3336
```
3437

3538
### Overloading
@@ -46,9 +49,32 @@ SELECT * FROM users WHERE name = '{{ name }}' AND surname = '{{ surname }}'
4649
SELECT * FROM users WHERE name = '{{ name }}' AND age = {{ age }}
4750
```
4851

49-
## Roadmap
52+
### Prevent SQL Injections
53+
54+
By default, SQL's are SQL injectable by default. But you are able to escape injectable parameters by writing `!` to the parameter.
55+
56+
```sql
57+
-- name: get_users(name : String, surname : String)
58+
59+
SELECT * FROM users WHERE name = '{{! name }}' AND surname = '{{! surname }}'
60+
```
61+
62+
### Composing SQLs
63+
64+
You can compose Tren methods easily to be DRY.
5065

51-
- Prevent SQL Injection.
66+
```sql
67+
-- name: filter_user(name : String, surname : String)
68+
69+
WHERE name = '{{! name }}' AND surname = '{{! surname }}'
70+
```
71+
72+
Let's reuse this now:
73+
```sql
74+
-- name: get_users(name : String, surname : String)
75+
76+
SELECT * FROM users {{ filter_user(name, surname) }}
77+
```
5278

5379
## Contributing
5480

spec/fixtures/composed.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- name: composition_1
2+
3+
where name = "fatih"
4+
5+
-- name: composition_2
6+
7+
select * from users {{ composition_1 }}

spec/fixtures/escape.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- name: escaped_users_without_parameters
2+
select * from users where name = 'fatih "fka" akin' limit 1
3+
4+
-- name: escaped_users_without_parameters_2
5+
select * from users where name = "hello" limit 1

spec/fixtures/glob/test.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- name: glob_1
2+
3+
select * from users
4+
5+
-- name: glob_2
6+
7+
select * from products

spec/fixtures/glob/test2.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- name: glob_3
2+
3+
select * from prices
4+
5+
-- name: glob_4
6+
7+
select * from companies

spec/fixtures/injection.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- name: injectable(name)
2+
select * from users where '{{ name }}'
3+
4+
-- name: protection(name)
5+
select * from users where '{{! name }}'

spec/tren_spec.cr

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Tren.load("#{__DIR__}/fixtures/test2.sql")
55
Tren.load("#{__DIR__}/fixtures/test3.sql")
66
Tren.load("#{__DIR__}/fixtures/multiline.sql")
77
Tren.load("#{__DIR__}/fixtures/multiple_multiline.sql")
8+
Tren.load("#{__DIR__}/fixtures/escape.sql")
9+
Tren.load("#{__DIR__}/fixtures/injection.sql")
10+
Tren.load("#{__DIR__}/fixtures/composed.sql")
11+
Tren.load("#{__DIR__}/fixtures/glob/**/*.sql")
812

913
describe Tren do
1014
it "should create and use method" do
@@ -27,10 +31,32 @@ describe Tren do
2731
get_user_info("Serdar \"sdogruyol\"", "Doğruyol").should eq("select * from users where name = 'Serdar \"sdogruyol\"' and name = 'Doğruyol'")
2832
end
2933

34+
it "should handle double quotes in sql file" do
35+
escaped_users_without_parameters.should eq("select * from users where name = 'fatih \"fka\" akin' limit 1")
36+
escaped_users_without_parameters_2.should eq("select * from users where name = \"hello\" limit 1")
37+
end
38+
3039
it "should handle multiline query" do
3140
multiline("kemal").should eq("select * from users\nwhere name = 'kemal'\nlimit 1")
3241
end
3342

43+
it "should escape parameters" do
44+
injectable("'; drop table users; --").should eq("select * from users where ''; drop table users; --'")
45+
protection("'; drop table users; --").should eq("select * from users where '\\'; drop table users; --'")
46+
end
47+
48+
it "should generate composed sqls" do
49+
composition_1.should eq("where name = \"fatih\"")
50+
composition_2.should eq("select * from users where name = \"fatih\"")
51+
end
52+
53+
it "should load glob files" do
54+
glob_1.should eq("select * from users")
55+
glob_2.should eq("select * from products")
56+
glob_3.should eq("select * from prices")
57+
glob_4.should eq("select * from companies")
58+
end
59+
3460
it "should handle multiple multiline queries" do
3561
multiline_one("kemal").should eq("select * from users\nwhere name = 'kemal'")
3662
multiline_two("kemal").should eq("select * from users\nwhere name = 'kemal'")

src/parser.cr

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
class Parser
2+
LINE_RE = /^\s*--\s*name:\s*([a-z\_\?\!0-9]+)(\(.*?\)|).*?\n/
3+
PARAM_RE = /\{\{(.*?)\}\}/
4+
PARAM_ESC_RE = /\{\{\!(.*?)\}\}/
5+
26
@metadata = ""
37
@sql_lines = [] of String
48

5-
def initialize
6-
@lines = File.read_lines(ARGV[0])
9+
def initialize(@lines : Array(String))
710
@metadata_index = 0
811
end
912

@@ -29,7 +32,7 @@ class Parser
2932
# checks if the given line contains metadata
3033
# example: -- name: get_users(name, surname)
3134
def metadata?(line)
32-
line.match(/^\s*-- name: ([a-z\_\?\!]+?\(.*?\)).*?\n/)
35+
line.match(LINE_RE)
3336
end
3437

3538
# checks for lines that is neither comment line (starts with -- )
@@ -39,25 +42,32 @@ class Parser
3942
end
4043

4144
def get_metadata(meta)
42-
meta.gsub(/^\s*-- name: ([a-z\_\?\!]+?\(.*?\)).*?\n/) do |token, match|
43-
match[1]
45+
meta.gsub(LINE_RE) do |token, match|
46+
"#{match[1]}#{match[2]}"
4447
end
4548
end
4649

4750
def parse_sql(sql)
48-
sql.gsub(/\{\{(.*?)\}\}/) do |token, match|
51+
sql = sql.gsub(PARAM_ESC_RE) do |token, match|
52+
"\#{Tren.escape(#{match[1]})}"
53+
end
54+
sql = sql.gsub(PARAM_RE) do |token, match|
4955
"\#{#{match[1]}}"
5056
end
5157
end
5258

53-
def define_method(metadata, sql)
54-
heredoc = <<-SQL
55-
#{sql}
56-
SQL
59+
def set_indent(sql)
60+
sql.lines.map do |line|
61+
" #{line}"
62+
end.join("").strip
63+
end
5764

65+
def define_method(metadata, sql)
5866
method = <<-METHOD
5967
def #{metadata}
60-
"#{heredoc}"
68+
<<-SQL
69+
#{set_indent(sql)}
70+
SQL
6171
end
6272
METHOD
6373
puts "#{method}"

src/reader.cr

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
require "./parser"
22

3-
Parser.new.parse
3+
Dir.glob(ARGV[0]).each do |file|
4+
lines = File.read_lines(file)
5+
Parser.new(lines).parse
6+
end

src/tren/utils.cr

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module Tren
2+
def self.escape(str : String)
3+
str.gsub(/\\|'/) { |c| "\\#{c}" }
4+
end
5+
end

0 commit comments

Comments
 (0)