Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
node-sqlite3
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
俞永鹏
node-sqlite3
Commits
aefb3cc4
Commit
aefb3cc4
authored
Sep 09, 2010
by
Orlando Vazquez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use a linked list for `statement.step`
parent
d8e7c186
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
102 additions
and
143 deletions
+102
-143
statement.cc
src/statement.cc
+75
-111
statement.h
src/statement.h
+22
-27
speedtest.js
tests/old/speedtest.js
+1
-1
test-statement-step.js
tests/test-statement-step.js
+4
-4
No files found.
src/statement.cc
View file @
aefb3cc4
...
@@ -479,7 +479,7 @@ int Statement::EIO_AfterStep(eio_req *req) {
...
@@ -479,7 +479,7 @@ int Statement::EIO_AfterStep(eio_req *req) {
HandleScope
scope
;
HandleScope
scope
;
Statement
*
sto
=
(
class
Statement
*
)
(
req
->
data
)
;
Statement
*
sto
=
(
class
Statement
*
)
req
->
data
;
sqlite3
*
db
=
sqlite3_db_handle
(
sto
->
stmt_
);
sqlite3
*
db
=
sqlite3_db_handle
(
sto
->
stmt_
);
Local
<
Value
>
argv
[
2
];
Local
<
Value
>
argv
[
2
];
...
@@ -498,39 +498,42 @@ int Statement::EIO_AfterStep(eio_req *req) {
...
@@ -498,39 +498,42 @@ int Statement::EIO_AfterStep(eio_req *req) {
else
{
else
{
Local
<
Object
>
row
=
Object
::
New
();
Local
<
Object
>
row
=
Object
::
New
();
for
(
int
i
=
0
;
i
<
sto
->
column_count_
;
i
++
)
{
struct
cell_node
*
cell
=
sto
->
cells
assert
(
sto
->
column_data_
);
,
*
next
=
NULL
;
if
(((
int
*
)
sto
->
column_types_
)[
i
]
!=
SQLITE_NULL
)
assert
(((
void
**
)
sto
->
column_data_
)[
i
]);
assert
(
sto
->
column_names_
[
i
]);
assert
(
sto
->
column_types_
[
i
]);
switch
(
sto
->
column_types_
[
i
])
{
for
(
int
i
=
0
;
cell
;
i
++
)
{
switch
(
cell
->
type
)
{
case
SQLITE_INTEGER
:
case
SQLITE_INTEGER
:
row
->
Set
(
String
::
NewSymbol
((
char
*
)
sto
->
column_names_
[
i
]),
row
->
Set
(
String
::
NewSymbol
((
char
*
)
sto
->
column_names_
[
i
]),
Int32
::
New
(
*
(
int
*
)
(
sto
->
column_data_
[
i
])));
Int32
::
New
(
*
(
int
*
)
cell
->
value
));
free
(
cell
->
value
);
break
;
break
;
case
SQLITE_FLOAT
:
case
SQLITE_FLOAT
:
row
->
Set
(
String
::
NewSymbol
(
sto
->
column_names_
[
i
]),
row
->
Set
(
String
::
NewSymbol
(
sto
->
column_names_
[
i
]),
Number
::
New
(
*
(
double
*
)
(
sto
->
column_data_
[
i
])));
Number
::
New
(
*
(
double
*
)
cell
->
value
));
free
(
cell
->
value
);
break
;
break
;
case
SQLITE_TEXT
:
case
SQLITE_TEXT
:
{
struct
string_t
*
str
=
(
struct
string_t
*
)
cell
->
value
;
// str->bytes-1 to compensate for the NULL terminator
row
->
Set
(
String
::
NewSymbol
(
sto
->
column_names_
[
i
]),
row
->
Set
(
String
::
NewSymbol
(
sto
->
column_names_
[
i
]),
String
::
New
((
char
*
)
(
sto
->
column_data_
[
i
])));
String
::
New
(
str
->
data
,
str
->
bytes
-
1
));
// don't free this pointer, it's owned by sqlite3
free
(
str
);
}
break
;
break
;
case
SQLITE_NULL
:
case
SQLITE_NULL
:
row
->
Set
(
String
::
New
(
sto
->
column_names_
[
i
]),
row
->
Set
(
String
::
New
(
sto
->
column_names_
[
i
]),
Local
<
Value
>::
New
(
Null
()));
Local
<
Value
>::
New
(
Null
()));
break
;
break
;
// no default
}
}
next
=
cell
->
next
;
free
(
cell
);
cell
=
next
;
}
}
argv
[
1
]
=
row
;
argv
[
1
]
=
row
;
}
}
...
@@ -563,63 +566,18 @@ int Statement::EIO_AfterStep(eio_req *req) {
...
@@ -563,63 +566,18 @@ int Statement::EIO_AfterStep(eio_req *req) {
void
Statement
::
FreeColumnData
(
void
)
{
void
Statement
::
FreeColumnData
(
void
)
{
if
(
!
column_count_
)
return
;
if
(
!
column_count_
)
return
;
for
(
int
i
=
0
;
i
<
column_count_
;
i
++
)
{
switch
(
column_types_
[
i
])
{
case
SQLITE_INTEGER
:
free
((
int
*
)
column_data_
[
i
]);
break
;
case
SQLITE_FLOAT
:
free
((
double
*
)
column_data_
[
i
]);
break
;
}
column_data_
[
i
]
=
NULL
;
}
free
(
column_names_
);
free
(
column_names_
);
free
(
column_types_
);
free
(
column_data_
);
column_count_
=
0
;
column_count_
=
0
;
column_types_
=
NULL
;
column_names_
=
NULL
;
column_names_
=
NULL
;
column_data_
=
NULL
;
}
}
void
Statement
::
InitializeColumns
(
void
)
{
void
Statement
::
InitializeColumns
(
void
)
{
this
->
column_count_
=
sqlite3_column_count
(
this
->
stmt_
);
this
->
column_count_
=
sqlite3_column_count
(
this
->
stmt_
);
assert
(
this
->
column_count_
);
this
->
column_names_
=
(
char
**
)
calloc
(
this
->
column_count_
,
sizeof
(
char
*
));
this
->
column_types_
=
(
int
*
)
calloc
(
this
->
column_count_
,
sizeof
(
int
));
this
->
column_names_
=
(
char
**
)
calloc
(
this
->
column_count_
,
sizeof
(
char
*
));
if
(
this
->
column_count_
)
{
this
->
column_data_
=
(
void
**
)
calloc
(
this
->
column_count_
,
sizeof
(
void
*
));
}
for
(
int
i
=
0
;
i
<
this
->
column_count_
;
i
++
)
{
for
(
int
i
=
0
;
i
<
this
->
column_count_
;
i
++
)
{
this
->
column_types_
[
i
]
=
sqlite3_column_type
(
this
->
stmt_
,
i
);
// Don't free this!
// Don't free this!
this
->
column_names_
[
i
]
=
(
char
*
)
sqlite3_column_name
(
this
->
stmt_
,
i
);
this
->
column_names_
[
i
]
=
(
char
*
)
sqlite3_column_name
(
this
->
stmt_
,
i
);
switch
(
this
->
column_types_
[
i
])
{
case
SQLITE_INTEGER
:
this
->
column_data_
[
i
]
=
(
int
*
)
malloc
(
sizeof
(
int
));
break
;
case
SQLITE_FLOAT
:
this
->
column_data_
[
i
]
=
(
double
*
)
malloc
(
sizeof
(
double
));
break
;
case
SQLITE_NULL
:
this
->
column_data_
[
i
]
=
NULL
;
break
;
// no need to allocate memory for strings
default
:
{
// unsupported type
}
}
}
}
}
}
...
@@ -645,55 +603,65 @@ int Statement::EIO_Step(eio_req *req) {
...
@@ -645,55 +603,65 @@ int Statement::EIO_Step(eio_req *req) {
sto
->
error_
=
false
;
sto
->
error_
=
false
;
if
(
rc
==
SQLITE_ROW
)
{
if
(
rc
==
SQLITE_ROW
)
{
// If these pointers are NULL, look up and store the number of columns
// If this pointer is NULL, look up and store the columns names.
// their names and types.
if
(
!
sto
->
column_names_
)
{
// Otherwise that means we have already looked up the column types and
// names so we can simply re-use that info.
if
(
!
sto
->
column_types_
&&
!
sto
->
column_names_
)
{
sto
->
InitializeColumns
();
sto
->
InitializeColumns
();
}
}
assert
(
sto
->
column_types_
&&
sto
->
column_data_
&&
sto
->
column_names_
);
struct
cell_node
*
cell_head
=
NULL
,
*
cell_prev
=
NULL
,
*
cell
=
NULL
;
for
(
int
i
=
0
;
i
<
sto
->
column_count_
;
i
++
)
{
for
(
int
i
=
0
;
i
<
sto
->
column_count_
;
i
++
)
{
int
type
=
sto
->
column_types_
[
i
];
cell
=
(
struct
cell_node
*
)
malloc
(
sizeof
(
struct
cell_node
));
// If this is the first cell, set `cell_head` to it, otherwise attach
// the new cell to the end of the list `cell_prev->next`.
(
!
cell_head
?
cell_head
:
cell_prev
->
next
)
=
cell
;
cell
->
type
=
sqlite3_column_type
(
sto
->
stmt_
,
i
);
cell
->
next
=
NULL
;
switch
(
type
)
{
switch
(
cell
->
type
)
{
case
SQLITE_INTEGER
:
case
SQLITE_INTEGER
:
*
(
int
*
)(
sto
->
column_data_
[
i
])
=
sqlite3_column_int
(
stmt
,
i
);
cell
->
value
=
(
int
*
)
malloc
(
sizeof
(
int
)
);
assert
(
sto
->
column_data_
[
i
]
);
*
(
int
*
)
cell
->
value
=
sqlite3_column_int
(
stmt
,
i
);
break
;
break
;
case
SQLITE_FLOAT
:
case
SQLITE_FLOAT
:
*
(
double
*
)(
sto
->
column_data_
[
i
])
=
sqlite3_column_double
(
stmt
,
i
);
cell
->
value
=
(
double
*
)
malloc
(
sizeof
(
double
));
*
(
double
*
)
cell
->
value
=
sqlite3_column_double
(
stmt
,
i
);
break
;
break
;
case
SQLITE_TEXT
:
{
case
SQLITE_TEXT
:
{
// It shouldn't be necessary to copy or free() this value,
char
*
text
=
(
char
*
)
sqlite3_column_text
(
stmt
,
i
);
// according to http://www.sqlite.org/c3ref/column_blob.html
int
size
=
1
+
sqlite3_column_bytes
(
stmt
,
i
);
// it will be reclaimed on the next step, reset, or finalize.
struct
string_t
*
str
=
(
struct
string_t
*
)
// I'm going to assume it's okay to keep this pointer around
malloc
(
sizeof
(
size_t
)
+
size
);
// until it is used in `EIO_AfterStep`
str
->
bytes
=
size
;
sto
->
column_data_
[
i
]
=
(
char
*
)
sqlite3_column_text
(
stmt
,
i
);
memcpy
(
str
->
data
,
text
,
size
);
cell
->
value
=
str
;
}
}
break
;
break
;
case
SQLITE_NULL
:
case
SQLITE_NULL
:
sto
->
column_data_
[
i
]
=
NULL
;
cell
->
value
=
NULL
;
break
;
break
;
default
:
{
default
:
{
assert
(
0
&&
"unsupported type"
);
assert
(
0
&&
"unsupported type"
);
}
}
if
(
cell
->
type
!=
SQLITE_NULL
)
{
assert
(
cell
->
value
);
}
}
if
(
sto
->
column_types_
[
i
]
!=
SQLITE_NULL
)
assert
(
sto
->
column_data_
[
i
]);
assert
(
sto
->
column_names_
[
i
]);
assert
(
sto
->
column_types_
[
i
]);
}
}
assert
(
sto
->
column_data_
);
cell_prev
=
cell
;
}
sto
->
cells
=
cell_head
;
assert
(
sto
->
column_names_
);
assert
(
sto
->
column_names_
);
assert
(
sto
->
column_types_
);
}
}
else
if
(
rc
==
SQLITE_DONE
)
{
else
if
(
rc
==
SQLITE_DONE
)
{
// nothing to do in this case
// nothing to do in this case
...
@@ -735,6 +703,7 @@ int Statement::EIO_AfterFetchAll(eio_req *req) {
...
@@ -735,6 +703,7 @@ int Statement::EIO_AfterFetchAll(eio_req *req) {
Statement
*
sto
=
fetchall_req
->
sto
;
Statement
*
sto
=
fetchall_req
->
sto
;
struct
row_node
*
cur
=
fetchall_req
->
rows
;
struct
row_node
*
cur
=
fetchall_req
->
rows
;
struct
cell_node
*
cell
=
NULL
;
Local
<
Value
>
argv
[
2
];
Local
<
Value
>
argv
[
2
];
...
@@ -750,7 +719,7 @@ int Statement::EIO_AfterFetchAll(eio_req *req) {
...
@@ -750,7 +719,7 @@ int Statement::EIO_AfterFetchAll(eio_req *req) {
for
(
int
row_count
=
0
;
cur
;
row_count
++
,
cur
=
cur
->
next
)
{
for
(
int
row_count
=
0
;
cur
;
row_count
++
,
cur
=
cur
->
next
)
{
Local
<
Object
>
row
=
Object
::
New
();
Local
<
Object
>
row
=
Object
::
New
();
struct
cell_node
*
cell
=
cur
->
cells
;
cell
=
cur
->
cells
;
// walk down the list
// walk down the list
for
(
int
i
=
0
;
cell
;
i
++
,
cell
=
cell
->
next
)
{
for
(
int
i
=
0
;
cell
;
i
++
,
cell
=
cell
->
next
)
{
...
@@ -881,54 +850,49 @@ int Statement::EIO_FetchAll(eio_req *req) {
...
@@ -881,54 +850,49 @@ int Statement::EIO_FetchAll(eio_req *req) {
,
&
ret
);
,
&
ret
);
cur
->
next
=
NULL
;
cur
->
next
=
NULL
;
if
(
!
head
)
{
// If this is the first row, set head to cur and hold it there since it
head
=
cur
;
// was the first result. Otherwise set the `next` field on the `prev`
}
// pointer to attach the newly allocated element.
else
{
(
!
head
?
head
:
prev
->
next
)
=
cur
;
prev
->
next
=
cur
;
}
struct
cell_node
*
cell_head
=
NULL
struct
cell_node
*
cell_head
=
NULL
,
*
cell_prev
=
NULL
,
*
cell_prev
=
NULL
,
*
cell
=
NULL
;
,
*
cell
=
NULL
;
for
(
int
i
=
0
;
i
<
sto
->
column_count_
;
i
++
)
{
for
(
int
i
=
0
;
i
<
sto
->
column_count_
;
i
++
)
{
cell
=
(
struct
cell_node
*
)
mpool_alloc
(
fetchall_req
->
pool
cell
=
(
struct
cell_node
*
)
,
sizeof
(
struct
cell_node
)
mpool_alloc
(
fetchall_req
->
pool
,
sizeof
(
struct
cell_node
),
&
ret
);
,
&
ret
);
if
(
!
cell_head
)
{
// If this is the first cell, set cell_head to cell and hold it there
cell_head
=
cell
;
// since it was the first result. Otherwise set the `next` field on the
}
// `prev` pointer to attach the newly allocated element.
else
{
(
!
cell_head
?
cell_head
:
cell_prev
->
next
)
=
cell
;
cell_prev
->
next
=
cell
;
}
cell
->
type
=
sqlite3_column_type
(
sto
->
stmt_
,
i
);
cell
->
type
=
sqlite3_column_type
(
sto
->
stmt_
,
i
);
cell
->
next
=
NULL
;
cell
->
next
=
NULL
;
switch
(
cell
->
type
)
{
switch
(
cell
->
type
)
{
case
SQLITE_INTEGER
:
case
SQLITE_INTEGER
:
cell
->
value
=
(
int
*
)
mpool_alloc
(
fetchall_req
->
pool
cell
->
value
=
(
int
*
)
,
sizeof
(
int
)
mpool_alloc
(
fetchall_req
->
pool
,
sizeof
(
int
)
,
&
ret
);
,
&
ret
);
*
(
int
*
)
cell
->
value
=
sqlite3_column_int
(
stmt
,
i
);
*
(
int
*
)
cell
->
value
=
sqlite3_column_int
(
stmt
,
i
);
break
;
break
;
case
SQLITE_FLOAT
:
case
SQLITE_FLOAT
:
cell
->
value
=
(
int
*
)
mpool_alloc
(
fetchall_req
->
pool
cell
->
value
=
(
double
*
)
,
sizeof
(
int
)
mpool_alloc
(
fetchall_req
->
pool
,
sizeof
(
double
)
,
&
ret
);
,
&
ret
);
*
(
double
*
)
cell
->
value
=
sqlite3_column_double
(
stmt
,
i
);
*
(
double
*
)
cell
->
value
=
sqlite3_column_double
(
stmt
,
i
);
break
;
break
;
case
SQLITE_TEXT
:
{
case
SQLITE_TEXT
:
{
char
*
text
=
(
char
*
)
sqlite3_column_text
(
stmt
,
i
);
char
*
text
=
(
char
*
)
sqlite3_column_text
(
stmt
,
i
);
int
size
=
1
+
sqlite3_column_bytes
(
stmt
,
i
);
int
size
=
1
+
sqlite3_column_bytes
(
stmt
,
i
);
struct
string_t
*
str
=
(
struct
string_t
*
)
struct
string_t
*
str
=
(
struct
string_t
*
)
mpool_alloc
(
fetchall_req
->
pool
mpool_alloc
(
fetchall_req
->
pool
,
sizeof
(
size_t
)
+
size
,
&
ret
);
,
sizeof
(
size_t
)
+
size
-
1
,
&
ret
);
str
->
bytes
=
size
;
str
->
bytes
=
size
;
memcpy
(
str
->
data
,
text
,
size
);
memcpy
(
str
->
data
,
text
,
size
);
cell
->
value
=
str
;
cell
->
value
=
str
;
...
...
src/statement.h
View file @
aefb3cc4
...
@@ -30,6 +30,24 @@ extern "C" {
...
@@ -30,6 +30,24 @@ extern "C" {
using
namespace
v8
;
using
namespace
v8
;
using
namespace
node
;
using
namespace
node
;
struct
cell_node
{
void
*
value
;
int
type
;
struct
cell_node
*
next
;
};
struct
row_node
{
struct
cell_node
*
cells
;
struct
row_node
*
next
;
};
// represent strings with this struct
struct
string_t
{
size_t
bytes
;
char
data
[];
};
class
Statement
:
public
EventEmitter
{
class
Statement
:
public
EventEmitter
{
public
:
public
:
...
@@ -43,16 +61,12 @@ class Statement : public EventEmitter {
...
@@ -43,16 +61,12 @@ class Statement : public EventEmitter {
Statement
(
sqlite3_stmt
*
stmt
,
int
first_rc
=
-
1
,
int
mode
=
0
)
Statement
(
sqlite3_stmt
*
stmt
,
int
first_rc
=
-
1
,
int
mode
=
0
)
:
EventEmitter
(),
first_rc_
(
first_rc
),
mode_
(
mode
),
stmt_
(
stmt
)
{
:
EventEmitter
(),
first_rc_
(
first_rc
),
mode_
(
mode
),
stmt_
(
stmt
)
{
column_count_
=
-
1
;
column_count_
=
-
1
;
column_types_
=
NULL
;
column_names_
=
NULL
;
column_names_
=
NULL
;
column_data_
=
NULL
;
}
}
~
Statement
()
{
~
Statement
()
{
if
(
stmt_
)
sqlite3_finalize
(
stmt_
);
if
(
stmt_
)
sqlite3_finalize
(
stmt_
);
if
(
column_types_
)
free
(
column_types_
);
if
(
column_names_
)
FreeColumnData
();
if
(
column_names_
)
free
(
column_names_
);
if
(
column_data_
)
FreeColumnData
();
}
}
static
Handle
<
Value
>
Bind
(
const
Arguments
&
args
);
static
Handle
<
Value
>
Bind
(
const
Arguments
&
args
);
...
@@ -87,14 +101,15 @@ class Statement : public EventEmitter {
...
@@ -87,14 +101,15 @@ class Statement : public EventEmitter {
private
:
private
:
int
column_count_
;
int
column_count_
;
int
*
column_types_
;
char
**
column_names_
;
char
**
column_names_
;
void
**
column_data_
;
bool
error_
;
bool
error_
;
int
first_rc_
;
int
first_rc_
;
int
mode_
;
int
mode_
;
sqlite3_stmt
*
stmt_
;
sqlite3_stmt
*
stmt_
;
// for statment.step
cell_node
*
cells
;
};
};
// indicates the key type (integer index or name string)
// indicates the key type (integer index or name string)
...
@@ -128,20 +143,6 @@ struct bind_pair {
...
@@ -128,20 +143,6 @@ struct bind_pair {
size_t
value_size
;
size_t
value_size
;
};
};
// Results will stored in a multi-dimensional linked list.
// That is, a linked list (rows) of linked lists (row values)
// Results are composed of rows. Rows are composed of cells.
struct
cell_node
{
void
*
value
;
int
type
;
struct
cell_node
*
next
;
};
struct
row_node
{
struct
cell_node
*
cells
;
struct
row_node
*
next
;
};
struct
fetchall_request
{
struct
fetchall_request
{
Persistent
<
Function
>
cb
;
Persistent
<
Function
>
cb
;
Statement
*
sto
;
Statement
*
sto
;
...
@@ -150,10 +151,4 @@ struct fetchall_request {
...
@@ -150,10 +151,4 @@ struct fetchall_request {
struct
row_node
*
rows
;
struct
row_node
*
rows
;
};
};
// represent strings with this struct
struct
string_t
{
size_t
bytes
;
char
data
[];
};
#endif
#endif
tests/old/speedtest.js
View file @
aefb3cc4
var
fs
=
require
(
"fs"
),
var
fs
=
require
(
"fs"
),
sys
=
require
(
"sys"
),
sys
=
require
(
"sys"
),
sqlite
=
require
(
"
../
sqlite"
);
sqlite
=
require
(
"sqlite"
);
var
puts
=
sys
.
puts
;
var
puts
=
sys
.
puts
;
var
inspect
=
sys
.
inspect
;
var
inspect
=
sys
.
inspect
;
...
...
tests/test-statement-step.js
View file @
aefb3cc4
...
@@ -25,14 +25,14 @@ function createTestTable(db, callback) {
...
@@ -25,14 +25,14 @@ function createTestTable(db, callback) {
var
testRows
=
[
[
1
,
"foo"
,
9
]
var
testRows
=
[
[
1
,
"foo"
,
9
]
,
[
2
,
"bar"
,
8
]
,
[
2
,
"bar"
,
8
]
,
[
3
,
"baz"
,
7
]
,
[
3
,
null
,
7
]
,
[
4
,
"quux"
,
6
]
,
[
4
,
"quux"
,
6
]
,
[
5
,
"juju"
,
5
]
,
[
5
,
"juju"
,
null
]
];
];
var
testRowsExpected
=
[
{
id
:
5
,
name
:
'juju'
,
age
:
5
}
var
testRowsExpected
=
[
{
id
:
5
,
name
:
'juju'
,
age
:
null
}
,
{
id
:
4
,
name
:
'quux'
,
age
:
6
}
,
{
id
:
4
,
name
:
'quux'
,
age
:
6
}
,
{
id
:
3
,
name
:
'baz'
,
age
:
7
}
,
{
id
:
3
,
name
:
null
,
age
:
7
}
,
{
id
:
2
,
name
:
'bar'
,
age
:
8
}
,
{
id
:
2
,
name
:
'bar'
,
age
:
8
}
,
{
id
:
1
,
name
:
'foo'
,
age
:
9
}
,
{
id
:
1
,
name
:
'foo'
,
age
:
9
}
];
];
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment