good morning!!!!
Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
U
upper
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package registry
Container registry
Harbor Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
This is an archived project. Repository and other project resources are read-only.
Show more breadcrumbs
open
upper
Commits
bd61cf84
Commit
bd61cf84
authored
Jul 5, 2015
by
José Carlos Nieto
Browse files
Options
Downloads
Patches
Plain Diff
Adding benchmarks for Find(), Remove(), Update() and equivalent SQL queries.
parent
f10e0a82
Branches
Branches containing commit
Tags
Tags containing commit
No related merge requests found
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
postgresql/Makefile
+1
-1
1 addition, 1 deletion
postgresql/Makefile
postgresql/benchmark_test.go
+205
-20
205 additions, 20 deletions
postgresql/benchmark_test.go
postgresql/collection.go
+2
-2
2 additions, 2 deletions
postgresql/collection.go
postgresql/database.go
+83
-96
83 additions, 96 deletions
postgresql/database.go
with
291 additions
and
119 deletions
postgresql/Makefile
+
1
−
1
View file @
bd61cf84
...
...
@@ -11,4 +11,4 @@ test: reset-db
$(
MAKE
)
-C
_example
bench
:
reset-db
go
test
-v
-test
.bench
=
.
go
test
-v
-test
.bench
=
.
-test
.benchtime
=
10s
-benchmem
This diff is collapsed.
Click to expand it.
postgresql/benchmark_test.go
+
205
−
20
View file @
bd61cf84
...
...
@@ -11,9 +11,17 @@ import (
)
const
(
testRows
=
1000
0
testRows
=
1000
)
func
updatedArtistN
(
i
int
)
string
{
return
fmt
.
Sprintf
(
"Updated Artist %d"
,
i
%
testRows
)
}
func
artistN
(
i
int
)
string
{
return
fmt
.
Sprintf
(
"Artist %d"
,
i
%
testRows
)
}
func
connectAndAddFakeRows
()
(
db
.
Database
,
error
)
{
var
err
error
var
sess
db
.
Database
...
...
@@ -24,12 +32,12 @@ func connectAndAddFakeRows() (db.Database, error) {
driver
:=
sess
.
Driver
()
.
(
*
sqlx
.
DB
)
if
_
,
err
=
driver
.
Exec
(
`TRUNCATE TABLE "artist"`
);
err
!=
nil
{
if
_
,
err
=
driver
.
Exec
(
`TRUNCATE TABLE "artist"
RESTART IDENTITY
`
);
err
!=
nil
{
return
nil
,
err
}
for
i
:=
0
;
i
<
testRows
;
i
++
{
if
_
,
err
=
driver
.
Exec
(
fmt
.
Sprintf
(
`INSERT INTO "artist" ("name") VALUES(
'Artist %d')`
,
i
));
err
!=
nil
{
if
_
,
err
=
driver
.
Exec
(
`INSERT INTO "artist" ("name") VALUES(
$1)`
,
artistN
(
i
));
err
!=
nil
{
return
nil
,
err
}
}
...
...
@@ -97,9 +105,9 @@ func BenchmarkSQLAppendWithArgs(b *testing.B) {
}
}
// BenchmarkSQLPrepareAppend benchmarks raw INSERT SQL queries using prepared
// BenchmarkSQLPrepare
d
Append benchmarks raw INSERT SQL queries using prepared
// statements but no arguments.
func
BenchmarkSQLPrepareAppend
(
b
*
testing
.
B
)
{
func
BenchmarkSQLPrepare
d
Append
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
...
...
@@ -131,7 +139,7 @@ func BenchmarkSQLPrepareAppend(b *testing.B) {
// BenchmarkSQLAppendWithArgs benchmarks raw INSERT SQL queries with arguments
// using prepared statements. The SQL query looks like the one that is
// generated by upper.io/db.
func
BenchmarkSQLPrepareAppendWithArgs
(
b
*
testing
.
B
)
{
func
BenchmarkSQLPrepare
d
AppendWithArgs
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
...
...
@@ -171,7 +179,7 @@ func BenchmarkSQLPrepareAppendWithArgs(b *testing.B) {
// BenchmarkSQLAppendWithVariableArgs benchmarks raw INSERT SQL queries with
// arguments using prepared statements. The SQL query looks like the one that
// is generated by upper.io/db.
func
BenchmarkSQLPrepareAppendWithVariableArgs
(
b
*
testing
.
B
)
{
func
BenchmarkSQLPrepare
d
AppendWithVariableArgs
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
...
...
@@ -207,10 +215,10 @@ func BenchmarkSQLPrepareAppendWithVariableArgs(b *testing.B) {
}
}
// BenchmarkSQLPrepareAppendTransactionWithArgs benchmarks raw INSERT queries
// BenchmarkSQLPrepare
d
AppendTransactionWithArgs benchmarks raw INSERT queries
// within a transaction block with arguments and prepared statements. SQL
// queries look like those generated by upper.io/db.
func
BenchmarkSQLPrepareAppendTransactionWithArgs
(
b
*
testing
.
B
)
{
func
BenchmarkSQLPrepare
d
AppendTransactionWithArgs
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
var
tx
*
sql
.
Tx
...
...
@@ -397,7 +405,7 @@ func BenchmarkUpperAppendTransactionWithMap(b *testing.B) {
}
}
// BenchmarkSQLSelect benchmarks SQL
select
queries.
// BenchmarkSQLSelect benchmarks SQL
SELECT
queries.
func
BenchmarkSQLSelect
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
...
...
@@ -414,7 +422,7 @@ func BenchmarkSQLSelect(b *testing.B) {
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
if
res
,
err
=
driver
.
Query
(
`SELECT * FROM "artist" WHERE "name" = $1`
,
fmt
.
Sprintf
(
"Artist %d"
,
i
%
testRows
));
err
!=
nil
{
if
res
,
err
=
driver
.
Query
(
`SELECT * FROM "artist" WHERE "name" = $1`
,
artistN
(
i
));
err
!=
nil
{
b
.
Fatal
(
err
)
}
res
.
Close
()
...
...
@@ -443,7 +451,7 @@ func BenchmarkSQLPreparedSelect(b *testing.B) {
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
if
res
,
err
=
stmt
.
Query
(
fmt
.
Sprintf
(
"Artist %d"
,
i
%
testRows
));
err
!=
nil
{
if
res
,
err
=
stmt
.
Query
(
artistN
(
i
));
err
!=
nil
{
b
.
Fatal
(
err
)
}
res
.
Close
()
...
...
@@ -466,27 +474,204 @@ func BenchmarkUpperFind(b *testing.B) {
b
.
Fatal
(
err
)
}
artist
.
Truncate
()
type
artistType
struct
{
Name
string
`db:"name"`
}
var
item
artistType
for
i
:=
0
;
i
<
testRows
;
i
++
{
item
=
artistType
{
Name
:
fmt
.
Sprintf
(
"Artist %d"
,
i
),
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
res
:=
artist
.
Find
(
db
.
Cond
{
"name"
:
artistN
(
i
)})
if
err
=
res
.
One
(
&
item
);
err
!=
nil
{
b
.
Fatal
(
err
)
}
if
_
,
err
=
artist
.
Append
(
item
);
err
!=
nil
{
}
}
// BenchmarkUpperFindAll benchmarks upper.io/db's All method.
func
BenchmarkUpperFindAll
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
artist
,
err
:=
sess
.
Collection
(
"artist"
)
if
err
!=
nil
{
b
.
Fatal
(
err
)
}
type
artistType
struct
{
Name
string
`db:"name"`
}
var
items
[]
artistType
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
res
:=
artist
.
Find
(
db
.
Cond
{
"name"
:
fmt
.
Sprintf
(
"Artist %d"
,
i
%
testRows
)})
if
err
=
res
.
One
(
&
item
);
err
!=
nil
{
res
:=
artist
.
Find
(
db
.
Or
{
db
.
Cond
{
"name"
:
artistN
(
i
)},
db
.
Cond
{
"name"
:
artistN
(
i
+
1
)},
db
.
Cond
{
"name"
:
artistN
(
i
+
2
)},
})
if
err
=
res
.
All
(
&
items
);
err
!=
nil
{
b
.
Fatal
(
err
)
}
if
len
(
items
)
!=
3
{
b
.
Fatal
(
"Expecting 3 results."
)
}
}
}
// BenchmarkSQLUpdate benchmarks SQL UPDATE queries.
func
BenchmarkSQLUpdate
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
driver
:=
sess
.
Driver
()
.
(
*
sqlx
.
DB
)
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
if
_
,
err
=
driver
.
Exec
(
`UPDATE "artist" SET "name" = $1 WHERE "name" = $2`
,
updatedArtistN
(
i
),
artistN
(
i
));
err
!=
nil
{
b
.
Fatal
(
err
)
}
}
}
// BenchmarkSQLPreparedUpdate benchmarks SQL UPDATE queries.
func
BenchmarkSQLPreparedUpdate
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
driver
:=
sess
.
Driver
()
.
(
*
sqlx
.
DB
)
stmt
,
err
:=
driver
.
Prepare
(
`UPDATE "artist" SET "name" = $1 WHERE "name" = $2`
)
if
err
!=
nil
{
b
.
Fatal
(
err
)
}
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
if
_
,
err
=
stmt
.
Exec
(
updatedArtistN
(
i
),
artistN
(
i
));
err
!=
nil
{
b
.
Fatal
(
err
)
}
}
}
// BenchmarkUpperUpdate benchmarks upper.io/db's Update method.
func
BenchmarkUpperUpdate
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
artist
,
err
:=
sess
.
Collection
(
"artist"
)
if
err
!=
nil
{
b
.
Fatal
(
err
)
}
type
artistType
struct
{
Name
string
`db:"name"`
}
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
newValue
:=
artistType
{
Name
:
updatedArtistN
(
i
),
}
res
:=
artist
.
Find
(
db
.
Cond
{
"name"
:
artistN
(
i
)})
if
err
=
res
.
Update
(
newValue
);
err
!=
nil
{
b
.
Fatal
(
err
)
}
}
}
// BenchmarkSQLDelete benchmarks SQL DELETE queries.
func
BenchmarkSQLDelete
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
driver
:=
sess
.
Driver
()
.
(
*
sqlx
.
DB
)
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
if
_
,
err
=
driver
.
Exec
(
`DELETE FROM "artist" WHERE "name" = $1`
,
artistN
(
i
));
err
!=
nil
{
b
.
Fatal
(
err
)
}
}
}
// BenchmarkSQLPreparedDelete benchmarks SQL DELETE queries.
func
BenchmarkSQLPreparedDelete
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
driver
:=
sess
.
Driver
()
.
(
*
sqlx
.
DB
)
stmt
,
err
:=
driver
.
Prepare
(
`DELETE FROM "artist" WHERE "name" = $1`
)
if
err
!=
nil
{
b
.
Fatal
(
err
)
}
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
if
_
,
err
=
stmt
.
Exec
(
artistN
(
i
));
err
!=
nil
{
b
.
Fatal
(
err
)
}
}
}
// BenchmarkUpperRemove benchmarks
func
BenchmarkUpperRemove
(
b
*
testing
.
B
)
{
var
err
error
var
sess
db
.
Database
if
sess
,
err
=
connectAndAddFakeRows
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
defer
sess
.
Close
()
artist
,
err
:=
sess
.
Collection
(
"artist"
)
if
err
!=
nil
{
b
.
Fatal
(
err
)
}
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
res
:=
artist
.
Find
(
db
.
Cond
{
"name"
:
artistN
(
i
)})
if
err
=
res
.
Remove
();
err
!=
nil
{
b
.
Fatal
(
err
)
}
}
...
...
This diff is collapsed.
Click to expand it.
postgresql/collection.go
+
2
−
2
View file @
bd61cf84
...
...
@@ -116,12 +116,12 @@ func (t *table) Append(item interface{}) (interface{}, error) {
return
nil
,
err
}
defer
rows
.
Close
()
keyMap
:=
map
[
string
]
interface
{}{}
if
err
:=
sqlutil
.
FetchRow
(
rows
,
&
keyMap
);
err
!=
nil
{
rows
.
Close
()
return
nil
,
err
}
rows
.
Close
()
// Does the item satisfy the db.IDSetter interface?
if
setter
,
ok
:=
item
.
(
db
.
IDSetter
);
ok
{
...
...
This diff is collapsed.
Click to expand it.
postgresql/database.go
+
83
−
96
View file @
bd61cf84
...
...
@@ -23,7 +23,6 @@ package postgresql
import
(
"database/sql"
"fmt"
"strconv"
"strings"
"time"
...
...
@@ -65,6 +64,50 @@ type columnSchemaT struct {
DataType
string
`db:"data_type"`
}
func
(
d
*
database
)
prepareStatement
(
stmt
*
sqlgen
.
Statement
)
(
p
*
sqlx
.
Stmt
,
query
string
,
err
error
)
{
if
d
.
session
==
nil
{
return
nil
,
""
,
db
.
ErrNotConnected
}
pc
,
ok
:=
d
.
cachedStatements
.
ReadRaw
(
stmt
)
if
ok
{
p
=
pc
.
(
*
sqlx
.
Stmt
)
}
else
{
query
=
compileAndReplacePlaceholders
(
stmt
)
if
d
.
tx
!=
nil
{
p
,
err
=
d
.
tx
.
Preparex
(
query
)
}
else
{
p
,
err
=
d
.
session
.
Preparex
(
query
)
}
if
err
!=
nil
{
return
nil
,
""
,
err
}
d
.
cachedStatements
.
Write
(
stmt
,
p
)
}
return
p
,
query
,
nil
}
func
compileAndReplacePlaceholders
(
stmt
*
sqlgen
.
Statement
)
(
query
string
)
{
buf
:=
stmt
.
Compile
(
template
.
Template
)
j
:=
1
for
i
:=
range
buf
{
if
buf
[
i
]
==
'?'
{
query
=
query
+
"$"
+
strconv
.
Itoa
(
j
)
j
++
}
else
{
query
=
query
+
string
(
buf
[
i
])
}
}
return
query
}
// Driver returns the underlying *sqlx.DB instance.
func
(
d
*
database
)
Driver
()
interface
{}
{
return
d
.
session
...
...
@@ -211,8 +254,6 @@ func (d *database) Collections() (collections []string, err error) {
return
nil
,
err
}
defer
rows
.
Close
()
collections
=
[]
string
{}
var
name
string
...
...
@@ -220,6 +261,7 @@ func (d *database) Collections() (collections []string, err error) {
for
rows
.
Next
()
{
// Getting table name.
if
err
=
rows
.
Scan
(
&
name
);
err
!=
nil
{
rows
.
Close
()
return
nil
,
err
}
...
...
@@ -291,124 +333,70 @@ func (d *database) Transaction() (db.Tx, error) {
// Exec compiles and executes a statement that does not return any rows.
func
(
d
*
database
)
Exec
(
stmt
*
sqlgen
.
Statement
,
args
...
interface
{})
(
sql
.
Result
,
error
)
{
var
query
string
var
res
sql
.
Resul
t
var
p
*
sqlx
.
Stm
t
var
err
error
var
start
,
end
int64
if
db
.
Debug
{
var
start
,
end
int64
start
=
time
.
Now
()
.
UnixNano
()
defer
func
()
{
end
=
time
.
Now
()
.
UnixNano
()
sqlutil
.
Log
(
query
,
args
,
err
,
start
,
end
)
}()
if
d
.
session
==
nil
{
return
nil
,
db
.
ErrNotConnected
}
query
=
stmt
.
Compile
(
template
.
Template
)
l
:=
len
(
args
)
for
i
:=
0
;
i
<
l
;
i
++
{
query
=
strings
.
Replace
(
query
,
`?`
,
fmt
.
Sprintf
(
`$%d`
,
i
+
1
),
1
)
}
if
d
.
tx
!=
nil
{
res
,
err
=
d
.
tx
.
Exec
(
query
,
args
...
)
}
else
{
res
,
err
=
d
.
session
.
Exec
(
query
,
args
...
)
if
p
,
query
,
err
=
d
.
prepareStatement
(
stmt
);
err
!=
nil
{
return
nil
,
err
}
return
res
,
err
return
p
.
Exec
(
args
...
)
}
// Query compiles and executes a statement that returns rows.
func
(
d
*
database
)
Query
(
stmt
*
sqlgen
.
Statement
,
args
...
interface
{})
(
*
sqlx
.
Rows
,
error
)
{
var
rows
*
sqlx
.
Rows
var
query
string
var
p
*
sqlx
.
Stmt
var
err
error
var
start
,
end
int64
if
db
.
Debug
{
var
start
,
end
int64
start
=
time
.
Now
()
.
UnixNano
()
defer
func
()
{
end
=
time
.
Now
()
.
UnixNano
()
sqlutil
.
Log
(
query
,
args
,
err
,
start
,
end
)
}()
if
d
.
session
==
nil
{
return
nil
,
db
.
ErrNotConnected
}
var
p
*
sqlx
.
Stmt
pc
,
ok
:=
d
.
cachedStatements
.
ReadRaw
(
stmt
)
if
ok
{
p
=
pc
.
(
*
sqlx
.
Stmt
)
}
else
{
buf
:=
stmt
.
Compile
(
template
.
Template
)
j
:=
1
for
i
:=
range
buf
{
if
buf
[
i
]
==
'?'
{
query
=
query
+
"$"
+
strconv
.
Itoa
(
j
)
j
++
}
else
{
query
=
query
+
string
(
buf
[
i
])
}
}
if
d
.
tx
!=
nil
{
p
,
err
=
d
.
tx
.
Preparex
(
query
)
}
else
{
p
,
err
=
d
.
session
.
Preparex
(
query
)
}
if
err
!=
nil
{
if
p
,
query
,
err
=
d
.
prepareStatement
(
stmt
);
err
!=
nil
{
return
nil
,
err
}
d
.
cachedStatements
.
Write
(
stmt
,
p
)
}
rows
,
err
=
p
.
Queryx
(
args
...
)
return
rows
,
err
return
p
.
Queryx
(
args
...
)
}
// QueryRow compiles and executes a statement that returns at most one row.
func
(
d
*
database
)
QueryRow
(
stmt
*
sqlgen
.
Statement
,
args
...
interface
{})
(
*
sqlx
.
Row
,
error
)
{
var
query
string
var
row
*
sqlx
.
Row
var
p
*
sqlx
.
Stmt
var
err
error
var
start
,
end
int64
if
db
.
Debug
{
var
start
,
end
int64
start
=
time
.
Now
()
.
UnixNano
()
defer
func
()
{
end
=
time
.
Now
()
.
UnixNano
()
sqlutil
.
Log
(
query
,
args
,
err
,
start
,
end
)
}()
if
d
.
session
==
nil
{
return
nil
,
db
.
ErrNotConnected
}
query
=
stmt
.
Compile
(
template
.
Template
)
l
:=
len
(
args
)
for
i
:=
0
;
i
<
l
;
i
++
{
query
=
strings
.
Replace
(
query
,
`?`
,
`$`
+
strconv
.
Itoa
(
i
+
1
),
1
)
}
if
d
.
tx
!=
nil
{
row
=
d
.
tx
.
QueryRowx
(
query
,
args
...
)
}
else
{
row
=
d
.
session
.
QueryRowx
(
query
,
args
...
)
if
p
,
query
,
err
=
d
.
prepareStatement
(
stmt
);
err
!=
nil
{
return
nil
,
err
}
return
row
,
err
return
p
.
QueryRowx
(
args
...
),
nil
}
// populateSchema looks up for the table info in the database and populates its
...
...
@@ -485,9 +473,8 @@ func (d *database) tableExists(names ...string) error {
return
db
.
ErrCollectionDoesNotExist
}
defer
rows
.
Close
()
if
!
rows
.
Next
()
{
rows
.
Close
()
return
db
.
ErrCollectionDoesNotExist
}
}
...
...
@@ -532,14 +519,15 @@ func (d *database) tableColumns(tableName string) ([]string, error) {
return
nil
,
err
}
defer
rows
.
Close
()
tableFields
:=
[]
columnSchemaT
{}
if
err
=
sqlutil
.
FetchRows
(
rows
,
&
tableFields
);
err
!=
nil
{
rows
.
Close
()
return
nil
,
err
}
rows
.
Close
()
d
.
schema
.
TableInfo
[
tableName
]
.
Columns
=
make
([]
string
,
0
,
len
(
tableFields
))
for
i
:=
range
tableFields
{
...
...
@@ -587,13 +575,12 @@ func (d *database) getPrimaryKey(tableName string) ([]string, error) {
return
nil
,
err
}
defer
rows
.
Close
()
tableSchema
.
PrimaryKey
=
make
([]
string
,
0
,
1
)
for
rows
.
Next
()
{
var
key
string
if
err
=
rows
.
Scan
(
&
key
);
err
!=
nil
{
rows
.
Close
()
return
nil
,
err
}
tableSchema
.
PrimaryKey
=
append
(
tableSchema
.
PrimaryKey
,
key
)
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment