Skip to content

Commit

Permalink
Merge pull request #7 from sdogruyol/glob_reader
Browse files Browse the repository at this point in the history
Add glob support to the loader.
  • Loading branch information
sdogruyol authored Oct 12, 2016
2 parents 2e51651 + 9321e61 commit a7c4872
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 14 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Require Tren and load your SQL file. It's going to create a first class method t
require "tren"
Tren.load("/path/to/your/file.sql")
# Or you can load multiple files at once:
Tren.load("./db/**/*.sql")
```

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

## Roadmap
### Prevent SQL Injections

By default, SQL's are SQL injectable by default. But you are able to escape injectable parameters by writing `!` to the parameter.

```sql
-- name: get_users(name : String, surname : String)
SELECT * FROM users WHERE name = '{{! name }}' AND surname = '{{! surname }}'
```

### Composing SQLs

You can compose Tren methods easily to be DRY.

- Prevent SQL Injection.
```sql
-- name: filter_user(name : String, surname : String)
WHERE name = '{{! name }}' AND surname = '{{! surname }}'
```

Let's reuse this now:
```sql
-- name: get_users(name : String, surname : String)
SELECT * FROM users {{ filter_user(name, surname) }}
```

## Contributing

Expand Down
7 changes: 7 additions & 0 deletions spec/fixtures/composed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- name: composition_1

where name = "fatih"

-- name: composition_2

select * from users {{ composition_1 }}
5 changes: 5 additions & 0 deletions spec/fixtures/escape.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- name: escaped_users_without_parameters
select * from users where name = 'fatih "fka" akin' limit 1

-- name: escaped_users_without_parameters_2
select * from users where name = "hello" limit 1
7 changes: 7 additions & 0 deletions spec/fixtures/glob/test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- name: glob_1

select * from users

-- name: glob_2

select * from products
7 changes: 7 additions & 0 deletions spec/fixtures/glob/test2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- name: glob_3

select * from prices

-- name: glob_4

select * from companies
5 changes: 5 additions & 0 deletions spec/fixtures/injection.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- name: injectable(name)
select * from users where '{{ name }}'

-- name: protection(name)
select * from users where '{{! name }}'
26 changes: 26 additions & 0 deletions spec/tren_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Tren.load("#{__DIR__}/fixtures/test2.sql")
Tren.load("#{__DIR__}/fixtures/test3.sql")
Tren.load("#{__DIR__}/fixtures/multiline.sql")
Tren.load("#{__DIR__}/fixtures/multiple_multiline.sql")
Tren.load("#{__DIR__}/fixtures/escape.sql")
Tren.load("#{__DIR__}/fixtures/injection.sql")
Tren.load("#{__DIR__}/fixtures/composed.sql")
Tren.load("#{__DIR__}/fixtures/glob/**/*.sql")

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

it "should handle double quotes in sql file" do
escaped_users_without_parameters.should eq("select * from users where name = 'fatih \"fka\" akin' limit 1")
escaped_users_without_parameters_2.should eq("select * from users where name = \"hello\" limit 1")
end

it "should handle multiline query" do
multiline("kemal").should eq("select * from users\nwhere name = 'kemal'\nlimit 1")
end

it "should escape parameters" do
injectable("'; drop table users; --").should eq("select * from users where ''; drop table users; --'")
protection("'; drop table users; --").should eq("select * from users where '\\'; drop table users; --'")
end

it "should generate composed sqls" do
composition_1.should eq("where name = \"fatih\"")
composition_2.should eq("select * from users where name = \"fatih\"")
end

it "should load glob files" do
glob_1.should eq("select * from users")
glob_2.should eq("select * from products")
glob_3.should eq("select * from prices")
glob_4.should eq("select * from companies")
end

it "should handle multiple multiline queries" do
multiline_one("kemal").should eq("select * from users\nwhere name = 'kemal'")
multiline_two("kemal").should eq("select * from users\nwhere name = 'kemal'")
Expand Down
32 changes: 21 additions & 11 deletions src/parser.cr
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
class Parser
LINE_RE = /^\s*--\s*name:\s*([a-z\_\?\!0-9]+)(\(.*?\)|).*?\n/
PARAM_RE = /\{\{(.*?)\}\}/
PARAM_ESC_RE = /\{\{\!(.*?)\}\}/

@metadata = ""
@sql_lines = [] of String

def initialize
@lines = File.read_lines(ARGV[0])
def initialize(@lines : Array(String))
@metadata_index = 0
end

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

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

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

def parse_sql(sql)
sql.gsub(/\{\{(.*?)\}\}/) do |token, match|
sql = sql.gsub(PARAM_ESC_RE) do |token, match|
"\#{Tren.escape(#{match[1]})}"
end
sql = sql.gsub(PARAM_RE) do |token, match|
"\#{#{match[1]}}"
end
end

def define_method(metadata, sql)
heredoc = <<-SQL
#{sql}
SQL
def set_indent(sql)
sql.lines.map do |line|
" #{line}"
end.join("").strip
end

def define_method(metadata, sql)
method = <<-METHOD
def #{metadata}
"#{heredoc}"
<<-SQL
#{set_indent(sql)}
SQL
end
METHOD
puts "#{method}"
Expand Down
5 changes: 4 additions & 1 deletion src/reader.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require "./parser"

Parser.new.parse
Dir.glob(ARGV[0]).each do |file|
lines = File.read_lines(file)
Parser.new(lines).parse
end
5 changes: 5 additions & 0 deletions src/tren/utils.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Tren
def self.escape(str : String)
str.gsub(/\\|'/) { |c| "\\#{c}" }
end
end

0 comments on commit a7c4872

Please sign in to comment.