Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gosqlite
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
gosqlite
Commits
0f1bd89b
Commit
0f1bd89b
authored
Oct 19, 2011
by
gwenn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor Scan functions.
parent
7627d184
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
239 additions
and
128 deletions
+239
-128
Makefile
Makefile
+2
-1
README
README
+20
-1
sqlite.go
sqlite.go
+123
-115
sqlite_test.go
sqlite_test.go
+6
-6
trace_test.go
trace_test.go
+5
-5
value.go
value.go
+83
-0
No files found.
Makefile
View file @
0f1bd89b
...
...
@@ -11,7 +11,8 @@ CGOFILES=\
backup.go
\
meta.go
\
trace.go
\
blob.go
blob.go
\
value.go
GOFILES
=
\
date.go
...
...
README
View file @
0f1bd89b
...
...
@@ -13,6 +13,25 @@ Stmt#Scan uses native sqlite3_column_x methods.
Stmt#NamedScan is added. It's compliant with [go-dbi](https://github.com/thomaslee/go-dbi/) API but I think its signature should be improved/modified.
Stmt#ScanColumn/NamedScanColumn are added to test NULL value.
Currently, the weak point of the binding is the *Scan methods:
The original implementation is using this strategy:
- convert the stored value to a []byte by calling sqlite3_column_bytes,
- convert the bytes to the desired Go type with correct feedback in case of illegal conversion,
- but apparently no support for NULL value.
Using the native sqlite3_column_x implies:
- optimal conversion from the storage type to Go type (when they match),
- loosy conversion when types mismatch (select cast('M' as int); --> 0),
- NULL value cannot be returned, default value (0, false, "") is returned instead.
Maybe we should let the caller do the conversion:
- she gives the Scan method a pointer/context (interface{}) and a callback function with this signature:
func (data interface{}, c *Converter) os.Error
- the Converter gives access to:
* the number of columns,
* the type of a column (by name or index),
* the bool/byte/[]byte/int/int64/float/string value of a column (by name or index).
- for each row, the callback is invoked.
Misc:
Conn#EnableFkey/IsFKeyEnabled
Conn#Changes/TotalChanges
...
...
@@ -25,7 +44,7 @@ Conn#EnableLoadExtension/LoadExtension
Stmt#ExecUpdate
Stmt#BindParameterCount/BindParameterIndex/BindParameterName
Stmt#ClearBindings
Stmt#ColumnCount/ColumnIndex(name)/ColumnName(index)
Stmt#ColumnCount/ColumnIndex(name)/ColumnName(index)
/ColumnType(index)
Stmt#ReadOnly
Blob:
...
...
sqlite.go
View file @
0f1bd89b
...
...
@@ -529,12 +529,34 @@ func (s *Stmt) ColumnName(index int) string {
return
C
.
GoString
(
C
.
sqlite3_column_name
(
s
.
stmt
,
C
.
int
(
index
)))
}
type
Type
int
func
(
t
Type
)
String
()
string
{
return
typeText
[
t
]
}
var
(
Integer
Type
=
Type
(
C
.
SQLITE_INTEGER
)
Float
Type
=
Type
(
C
.
SQLITE_FLOAT
)
Blob
Type
=
Type
(
C
.
SQLITE_BLOB
)
Null
Type
=
Type
(
C
.
SQLITE_NULL
)
Text
Type
=
Type
(
C
.
SQLITE3_TEXT
)
)
var
typeText
=
map
[
Type
]
string
{
Integer
:
"Integer"
,
Float
:
"Float"
,
Blob
:
"Blob"
,
Null
:
"Null"
,
Text
:
"Text"
,
}
// The leftmost column is number 0.
// After a type conversion, the value returned by sqlite3_column_type() is undefined.
// Calls sqlite3_column_type
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
columnType
(
index
int
)
C
.
int
{
return
C
.
sqlite3_column_type
(
s
.
stmt
,
C
.
int
(
index
))
func
(
s
*
Stmt
)
ColumnType
(
index
int
)
Type
{
return
Type
(
C
.
sqlite3_column_type
(
s
.
stmt
,
C
.
int
(
index
)
))
}
// NULL value is converted to 0 if arg type is *int,*int64,*float,*float64, to "" for *string, to []byte{} for *[]byte and to false for *bool.
...
...
@@ -554,7 +576,7 @@ func (s *Stmt) NamedScan(args ...interface{}) os.Error {
return
err
}
ptr
:=
args
[
i
+
1
]
_
,
err
=
s
.
ScanColumn
(
index
,
ptr
,
false
)
_
,
err
=
s
.
ScanColumn
(
index
,
ptr
/*, false*/
)
if
err
!=
nil
{
return
err
}
...
...
@@ -573,42 +595,10 @@ func (s *Stmt) Scan(args ...interface{}) os.Error {
}
for
i
,
v
:=
range
args
{
_
,
err
:=
s
.
ScanColumn
(
i
,
v
,
false
)
if
err
!=
nil
{
return
err
}
}
return
nil
}
// Calls sqlite3_column_count and sqlite3_column_(blob|double|int|int64|text) depending on columns type.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanNamedValues
(
values
...
NamedValue
)
os
.
Error
{
n
:=
s
.
ColumnCount
()
if
n
!=
len
(
values
)
{
// What happens when the number of arguments is less than the number of columns?
return
os
.
NewError
(
fmt
.
Sprintf
(
"incorrect argument count for Stmt.ScanValues: have %d want %d"
,
len
(
values
),
n
))
}
for
_
,
v
:=
range
values
{
index
,
err
:=
s
.
ColumnIndex
(
v
.
Name
())
// How to look up only once for one statement ?
_
,
err
:=
s
.
ScanColumn
(
i
,
v
/*, false*/
)
if
err
!=
nil
{
return
err
}
s
.
ScanValue
(
index
,
v
)
}
return
nil
}
// Calls sqlite3_column_count and sqlite3_column_(blob|double|int|int64|text) depending on columns type.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanValues
(
values
...
Value
)
os
.
Error
{
n
:=
s
.
ColumnCount
()
if
n
!=
len
(
values
)
{
// What happens when the number of arguments is less than the number of columns?
return
os
.
NewError
(
fmt
.
Sprintf
(
"incorrect argument count for Stmt.ScanValues: have %d want %d"
,
len
(
values
),
n
))
}
for
i
,
v
:=
range
values
{
s
.
ScanValue
(
i
,
v
)
}
return
nil
}
...
...
@@ -637,120 +627,138 @@ func (s *Stmt) ColumnIndex(name string) (int, os.Error) {
}
// Set nullable to false to skip NULL type test.
// Returns true when
nullable is true and
column is null.
// Returns true when column is null.
// Calls sqlite3_column_count, sqlite3_column_name and sqlite3_column_(blob|double|int|int64|text) depending on args type.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
NamedScanColumn
(
name
string
,
value
interface
{}
,
nullable
bool
)
(
bool
,
os
.
Error
)
{
func
(
s
*
Stmt
)
NamedScanColumn
(
name
string
,
value
interface
{})
(
bool
,
os
.
Error
)
{
index
,
err
:=
s
.
ColumnIndex
(
name
)
if
err
!=
nil
{
return
false
,
err
}
return
s
.
ScanColumn
(
index
,
value
,
true
)
return
s
.
ScanColumn
(
index
,
value
)
}
// The leftmost column/index is number 0.
// Set nullable to false to skip NULL type test.
// Returns true when nullable is true and column is null.
// Returns true when column is null.
// Calls sqlite3_column_(blob|double|int|int64|text) depending on args type.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanColumn
(
index
int
,
value
interface
{}
,
nullable
bool
)
(
bool
,
os
.
Error
)
{
func
(
s
*
Stmt
)
ScanColumn
(
index
int
,
value
interface
{})
(
bool
,
os
.
Error
)
{
var
isNull
bool
switch
value
:=
value
.
(
type
)
{
case
nil
:
case
*
string
:
p
:=
C
.
sqlite3_column_text
(
s
.
stmt
,
C
.
int
(
index
))
if
p
==
nil
{
*
value
=
""
isNull
=
true
}
else
{
n
:=
C
.
sqlite3_column_bytes
(
s
.
stmt
,
C
.
int
(
index
))
*
value
=
C
.
GoStringN
((
*
C
.
char
)(
unsafe
.
Pointer
(
p
)),
n
)
}
*
value
,
isNull
=
s
.
ScanText
(
index
)
case
*
int
:
if
nullable
&&
s
.
columnType
(
index
)
==
C
.
SQLITE_NULL
{
*
value
=
0
isNull
=
true
}
else
{
*
value
=
int
(
C
.
sqlite3_column_int
(
s
.
stmt
,
C
.
int
(
index
)))
}
*
value
,
isNull
=
s
.
ScanInt
(
index
)
case
*
int64
:
if
nullable
&&
s
.
columnType
(
index
)
==
C
.
SQLITE_NULL
{
*
value
=
0
isNull
=
true
}
else
{
*
value
=
int64
(
C
.
sqlite3_column_int64
(
s
.
stmt
,
C
.
int
(
index
)))
}
*
value
,
isNull
=
s
.
ScanInt64
(
index
)
case
*
byte
:
if
nullable
&&
s
.
columnType
(
index
)
==
C
.
SQLITE_NULL
{
*
value
=
0
isNull
=
true
}
else
{
*
value
=
byte
(
C
.
sqlite3_column_int
(
s
.
stmt
,
C
.
int
(
index
)))
}
*
value
,
isNull
=
s
.
ScanByte
(
index
)
case
*
bool
:
if
nullable
&&
s
.
columnType
(
index
)
==
C
.
SQLITE_NULL
{
*
value
=
false
isNull
=
true
}
else
{
*
value
=
C
.
sqlite3_column_int
(
s
.
stmt
,
C
.
int
(
index
))
==
1
}
*
value
,
isNull
=
s
.
ScanBool
(
index
)
case
*
float64
:
if
nullable
&&
s
.
columnType
(
index
)
==
C
.
SQLITE_NULL
{
*
value
=
0
isNull
=
true
}
else
{
*
value
=
float64
(
C
.
sqlite3_column_double
(
s
.
stmt
,
C
.
int
(
index
)))
}
*
value
,
isNull
=
s
.
ScanFloat64
(
index
)
case
*
[]
byte
:
p
:=
C
.
sqlite3_column_blob
(
s
.
stmt
,
C
.
int
(
index
))
if
p
==
nil
{
*
value
=
nil
isNull
=
true
}
else
{
n
:=
C
.
sqlite3_column_bytes
(
s
.
stmt
,
C
.
int
(
index
))
*
value
=
(
*
[
1
<<
30
]
byte
)(
unsafe
.
Pointer
(
p
))[
0
:
n
]
}
*
value
,
isNull
=
s
.
ScanBlob
(
index
)
default
:
return
false
,
os
.
NewError
(
"unsupported type in Scan: "
+
reflect
.
TypeOf
(
value
)
.
String
())
}
return
isNull
,
nil
}
type
NamedValue
interface
{
Value
Name
()
string
// The leftmost column/index is number 0.
// Returns true when column is null.
// Calls sqlite3_column_text.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanText
(
index
int
)
(
value
string
,
isNull
bool
)
{
p
:=
C
.
sqlite3_column_text
(
s
.
stmt
,
C
.
int
(
index
))
if
p
==
nil
{
isNull
=
true
}
else
{
n
:=
C
.
sqlite3_column_bytes
(
s
.
stmt
,
C
.
int
(
index
))
value
=
C
.
GoStringN
((
*
C
.
char
)(
unsafe
.
Pointer
(
p
)),
n
)
}
return
}
type
Value
interface
{
setNull
(
bool
)
setInt
(
int64
)
// Versus int?
setFloat
(
float64
)
setText
(
string
)
setBlob
([]
byte
)
// The leftmost column/index is number 0.
// Returns true when column is null.
// Calls sqlite3_column_int.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanInt
(
index
int
)
(
value
int
,
isNull
bool
)
{
if
s
.
ColumnType
(
index
)
==
Null
{
// TODO How to avoid this test for not nullable column or when it doesn't care
isNull
=
true
}
else
{
value
=
int
(
C
.
sqlite3_column_int
(
s
.
stmt
,
C
.
int
(
index
)))
}
return
}
// The leftmost column/index is number 0.
// Calls sqlite3_column_(blob|double|int|int64|text) depending on columns type.
// Returns true when column is null.
// Calls sqlite3_column_int64.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanValue
(
index
int
,
value
Value
)
{
switch
s
.
columnType
(
index
)
{
case
C
.
SQLITE_NULL
:
value
.
setNull
(
true
)
case
C
.
SQLITE_TEXT
:
p
:=
C
.
sqlite3_column_text
(
s
.
stmt
,
C
.
int
(
index
))
n
:=
C
.
sqlite3_column_bytes
(
s
.
stmt
,
C
.
int
(
index
))
value
.
setText
(
C
.
GoStringN
((
*
C
.
char
)(
unsafe
.
Pointer
(
p
)),
n
))
case
C
.
SQLITE_INTEGER
:
value
.
setInt
(
int64
(
C
.
sqlite3_column_int64
(
s
.
stmt
,
C
.
int
(
index
))))
case
C
.
SQLITE_FLOAT
:
value
.
setFloat
(
float64
(
C
.
sqlite3_column_double
(
s
.
stmt
,
C
.
int
(
index
))))
case
C
.
SQLITE_BLOB
:
p
:=
C
.
sqlite3_column_blob
(
s
.
stmt
,
C
.
int
(
index
))
func
(
s
*
Stmt
)
ScanInt64
(
index
int
)
(
value
int64
,
isNull
bool
)
{
if
s
.
ColumnType
(
index
)
==
Null
{
// TODO How to avoid this test ...
isNull
=
true
}
else
{
value
=
int64
(
C
.
sqlite3_column_int64
(
s
.
stmt
,
C
.
int
(
index
)))
}
return
}
// The leftmost column/index is number 0.
// Returns true when column is null.
// Calls sqlite3_column_int.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanByte
(
index
int
)
(
value
byte
,
isNull
bool
)
{
if
s
.
ColumnType
(
index
)
==
Null
{
// TODO How to avoid this test ...
isNull
=
true
}
else
{
value
=
byte
(
C
.
sqlite3_column_int
(
s
.
stmt
,
C
.
int
(
index
)))
}
return
}
// The leftmost column/index is number 0.
// Returns true when column is null.
// Calls sqlite3_column_int.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanBool
(
index
int
)
(
value
bool
,
isNull
bool
)
{
if
s
.
ColumnType
(
index
)
==
Null
{
// TODO How to avoid this test ...
isNull
=
true
}
else
{
value
=
C
.
sqlite3_column_int
(
s
.
stmt
,
C
.
int
(
index
))
==
1
}
return
}
// The leftmost column/index is number 0.
// Returns true when column is null.
// Calls sqlite3_column_double.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanFloat64
(
index
int
)
(
value
float64
,
isNull
bool
)
{
if
s
.
ColumnType
(
index
)
==
Null
{
// TODO How to avoid this test ...
isNull
=
true
}
else
{
value
=
float64
(
C
.
sqlite3_column_double
(
s
.
stmt
,
C
.
int
(
index
)))
}
return
}
// The leftmost column/index is number 0.
// Returns true when column is null.
// Calls sqlite3_column_bytes.
// http://sqlite.org/c3ref/column_blob.html
func
(
s
*
Stmt
)
ScanBlob
(
index
int
)
(
value
[]
byte
,
isNull
bool
)
{
p
:=
C
.
sqlite3_column_blob
(
s
.
stmt
,
C
.
int
(
index
))
if
p
==
nil
{
isNull
=
true
}
else
{
n
:=
C
.
sqlite3_column_bytes
(
s
.
stmt
,
C
.
int
(
index
))
value
.
setBlob
((
*
[
1
<<
30
]
byte
)(
unsafe
.
Pointer
(
p
))[
0
:
n
])
default
:
panic
(
"The column type is not one of SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB, or SQLITE_NULL"
)
value
=
(
*
[
1
<<
30
]
byte
)(
unsafe
.
Pointer
(
p
))[
0
:
n
]
}
return
}
// Calls http://sqlite.org/c3ref/finalize.html
...
...
sqlite_test.go
View file @
0f1bd89b
...
...
@@ -315,19 +315,19 @@ func TestScanColumn(t *testing.T) {
t
.
Fatal
(
"no result"
)
}
var
i1
,
i2
,
i3
int
null
:=
Must
(
s
.
ScanColumn
(
0
,
&
i1
,
true
))
null
:=
Must
(
s
.
ScanColumn
(
0
,
&
i1
/*, true*/
))
if
null
{
t
.
Errorf
(
"Expected not null value"
)
}
else
if
i1
!=
1
{
t
.
Errorf
(
"Expected 1 <> %d
\n
"
,
i1
)
}
null
=
Must
(
s
.
ScanColumn
(
1
,
&
i2
,
true
))
null
=
Must
(
s
.
ScanColumn
(
1
,
&
i2
/*, true*/
))
if
!
null
{
t
.
Errorf
(
"Expected null value"
)
}
else
if
i2
!=
0
{
t
.
Errorf
(
"Expected 0 <> %d
\n
"
,
i2
)
}
null
=
Must
(
s
.
ScanColumn
(
2
,
&
i3
,
true
))
null
=
Must
(
s
.
ScanColumn
(
2
,
&
i3
/*, true*/
))
if
null
{
t
.
Errorf
(
"Expected not null value"
)
}
else
if
i3
!=
0
{
...
...
@@ -348,19 +348,19 @@ func TestNamedScanColumn(t *testing.T) {
t
.
Fatal
(
"no result"
)
}
var
i1
,
i2
,
i3
int
null
:=
Must
(
s
.
NamedScanColumn
(
"i1"
,
&
i1
,
true
))
null
:=
Must
(
s
.
NamedScanColumn
(
"i1"
,
&
i1
/*, true*/
))
if
null
{
t
.
Errorf
(
"Expected not null value"
)
}
else
if
i1
!=
1
{
t
.
Errorf
(
"Expected 1 <> %d
\n
"
,
i1
)
}
null
=
Must
(
s
.
NamedScanColumn
(
"i2"
,
&
i2
,
true
))
null
=
Must
(
s
.
NamedScanColumn
(
"i2"
,
&
i2
/*, true*/
))
if
!
null
{
t
.
Errorf
(
"Expected null value"
)
}
else
if
i2
!=
0
{
t
.
Errorf
(
"Expected 0 <> %d
\n
"
,
i2
)
}
null
=
Must
(
s
.
NamedScanColumn
(
"i3"
,
&
i3
,
true
))
null
=
Must
(
s
.
NamedScanColumn
(
"i3"
,
&
i3
/*, true*/
))
if
null
{
t
.
Errorf
(
"Expected not null value"
)
}
else
if
i3
!=
0
{
...
...
trace_test.go
View file @
0f1bd89b
package
sqlite_test
import
(
"fmt"
//
"fmt"
.
"github.com/gwenn/gosqlite"
"testing"
)
func
trace
(
d
interface
{},
t
string
)
{
fmt
.
Printf
(
"%s: %s
\n
"
,
d
,
t
)
//
fmt.Printf("%s: %s\n", d, t)
}
func
authorizer
(
d
interface
{},
action
Action
,
arg1
,
arg2
,
arg3
,
arg4
string
)
Auth
{
fmt
.
Printf
(
"%s: %d, %s, %s, %s, %s
\n
"
,
d
,
action
,
arg1
,
arg2
,
arg3
,
arg4
)
//
fmt.Printf("%s: %d, %s, %s, %s, %s\n", d, action, arg1, arg2, arg3, arg4)
return
AUTH_OK
}
func
profile
(
d
interface
{},
sql
string
,
nanoseconds
uint64
)
{
fmt
.
Printf
(
"%s: %s = %d
\n
"
,
d
,
sql
,
nanoseconds
/
1000
)
//
fmt.Printf("%s: %s = %d\n", d, sql, nanoseconds/1000)
}
func
progressHandler
(
d
interface
{})
int
{
fmt
.
Print
(
"+"
)
//
fmt.Print("+")
return
0
}
...
...
value.go
0 → 100644
View file @
0f1bd89b
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package sqlite provides access to the SQLite library, version 3.
package
sqlite
/*
#include <sqlite3.h>
#include <stdlib.h>
*/
import
"C"
/*
import (
"fmt"
"os"
"unsafe"
)
*/
// Calls sqlite3_column_count and sqlite3_column_(blob|double|int|int64|text) depending on columns type.
// http://sqlite.org/c3ref/column_blob.html
/*
func (s *Stmt) ScanNamedValues(values ...NamedValue) os.Error {
n := s.ColumnCount()
if n != len(values) { // What happens when the number of arguments is less than the number of columns?
return os.NewError(fmt.Sprintf("incorrect argument count for Stmt.ScanValues: have %d want %d", len(values), n))
}
for _, v := range values {
index, err := s.ColumnIndex(v.Name()) // How to look up only once for one statement ?
if err != nil {
return err
}
s.ScanValue(index, v)
}
return nil
}
*/
// Calls sqlite3_column_count and sqlite3_column_(blob|double|int|int64|text) depending on columns type.
// http://sqlite.org/c3ref/column_blob.html
/*
func (s *Stmt) ScanValues(values ...Value) os.Error {
n := s.ColumnCount()
if n != len(values) { // What happens when the number of arguments is less than the number of columns?
return os.NewError(fmt.Sprintf("incorrect argument count for Stmt.ScanValues: have %d want %d", len(values), n))
}
for i, v := range values {
s.ScanValue(i, v)
}
return nil
}
*/
// The leftmost column/index is number 0.
// Calls sqlite3_column_(blob|double|int|int64|text) depending on columns type.
// http://sqlite.org/c3ref/column_blob.html
/*
func (s *Stmt) ScanValue(index int) {
switch s.columnType(index) {
case C.SQLITE_NULL:
value.setNull(true)
case C.SQLITE_TEXT:
p := C.sqlite3_column_text(s.stmt, C.int(index))
n := C.sqlite3_column_bytes(s.stmt, C.int(index))
value.setText(C.GoStringN((*C.char)(unsafe.Pointer(p)), n))
case C.SQLITE_INTEGER:
value.setInt(int64(C.sqlite3_column_int64(s.stmt, C.int(index))))
case C.SQLITE_FLOAT:
value.setFloat(float64(C.sqlite3_column_double(s.stmt, C.int(index))))
case C.SQLITE_BLOB:
p := C.sqlite3_column_blob(s.stmt, C.int(index))
n := C.sqlite3_column_bytes(s.stmt, C.int(index))
value.setBlob((*[1 << 30]byte)(unsafe.Pointer(p))[0:n])
default:
panic("The column type is not one of SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB, or SQLITE_NULL")
}
}
*/
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