Skip to content
GitLab
Explore
Projects
Groups
Snippets
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Memri
POD
Commits
0357587d
Unverified
Commit
0357587d
authored
4 years ago
by
Vasili Novikov
Browse files
Options
Download
Email Patches
Plain Diff
Use strict database type
E.g. forbid items with incorrect type to be inserted in the database
parent
5d3718b7
Pipeline
#1081
failed with stage
in 12 minutes and 26 seconds
Changes
4
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
README.md
+5
-0
README.md
src/database_migrate_schema.rs
+57
-28
src/database_migrate_schema.rs
src/internal_api.rs
+1
-1
src/internal_api.rs
src/sql_converters.rs
+63
-20
src/sql_converters.rs
with
126 additions
and
49 deletions
+126
-49
README.md
+
5
-
0
View file @
0357587d
...
...
@@ -106,6 +106,11 @@ to the clients, however, and clients should only ever receive/send `true` and `f
Use this database type to denote DateTime.
Internally stored as Integer and should be passed as Integer.
All column definitions of the same case-insensitive name MUST have the same type and indexing.
All column names MUST consist of
`a-zA-Z_`
characters only, and start with
`a-zA-Z`
.
All type names MUST consist of
`a-zA-Z_`
characters only,
and start with
`a-zA-Z`
(same as column names).
### Changing the schema locally
If you want to make local changes to the schema while developing
new functionality, you can edit the schema directly.
...
...
This diff is collapsed.
Click to expand it.
src/database_migrate_schema.rs
+
57
-
28
View file @
0357587d
...
...
@@ -11,15 +11,6 @@ use std::collections::HashMap;
use
std
::
collections
::
HashSet
;
use
std
::
hash
::
Hash
;
/// Constraints:
///
/// * All column definitions of the same case-insensitive name MUST have the same type and indexing
///
/// * All column names MUST consist of `a-zA-Z_` characters only,
/// and start with `a-zA-Z`
///
/// * All type names MUST consist of `a-zA-Z_` characters only,
/// and start with `a-zA-Z` (same as column names)
#[derive(Serialize,
Deserialize)]
struct
DatabaseSchema
{
types
:
Vec
<
DatabaseType
>
,
...
...
@@ -41,7 +32,7 @@ struct DatabaseColumn {
/// See `README.md#understanding-the-schema` to understand possible
/// property types and their meaning
#[derive(Serialize,
Deserialize,
Debug,
Copy,
Clone,
PartialEq)]
enum
DatabaseColumnType
{
pub
enum
DatabaseColumnType
{
Text
,
Integer
,
Real
,
...
...
@@ -49,28 +40,75 @@ enum DatabaseColumnType {
DateTime
,
}
fn
get_columns_of_type
(
dbtype
:
DatabaseColumnType
)
->
HashSet
<
String
>
{
let
parsed_schema
:
DatabaseSchema
=
serde_json
::
from_slice
(
AUTOGENERATED_SCHEMA
)
.expect
(
"Failed to parse autogenerated_database_schema to JSON"
);
let
columns
=
parsed_schema
.types
.iter
()
.flat_map
(|
t
|
&
t
.properties
);
let
mut
result
=
HashSet
::
new
();
for
column
in
columns
{
if
column
.dbtype
==
dbtype
{
result
.insert
(
column
.name
.to_string
());
}
}
result
}
lazy_static!
{
pub
static
ref
TEXT_COLUMNS
:
HashSet
<
String
>
=
{
let
mut
result
=
get_columns_of_type
(
DatabaseColumnType
::
Text
);
result
.insert
(
"_type"
.to_string
());
result
};
}
lazy_static!
{
pub
static
ref
INTEGER_COLUMNS
:
HashSet
<
String
>
=
{
let
mut
result
=
get_columns_of_type
(
DatabaseColumnType
::
Integer
);
result
.insert
(
"uid"
.to_string
());
result
.insert
(
"version"
.to_string
());
result
};
}
lazy_static!
{
pub
static
ref
REAL_COLUMNS
:
HashSet
<
String
>
=
get_columns_of_type
(
DatabaseColumnType
::
Real
);
}
lazy_static!
{
pub
static
ref
BOOL_COLUMNS
:
HashSet
<
String
>
=
{
let
mut
result
=
get_columns_of_type
(
DatabaseColumnType
::
Bool
);
result
.insert
(
"deleted"
.to_string
());
result
};
}
lazy_static!
{
pub
static
ref
DATE_TIME_COLUMNS
:
HashSet
<
String
>
=
{
let
mut
result
=
get_columns_of_type
(
DatabaseColumnType
::
DateTime
);
result
.insert
(
"dateCreated"
.to_string
());
result
.insert
(
"dateModified"
.to_string
());
result
};
}
lazy_static!
{
pub
static
ref
ALL_COLUMN_TYPES
:
HashMap
<
String
,
DatabaseColumnType
>
=
{
let
parsed_schema
:
DatabaseSchema
=
serde_json
::
from_slice
(
AUTOGENERATED_SCHEMA
)
.expect
(
"Failed to parse autogenerated_database_schema to JSON"
);
let
columns
=
parsed_schema
.types
.iter
()
.flat_map
(|
t
|
&
t
.properties
);
let
mut
result
=
HashSet
::
new
();
for
column
in
columns
{
if
column
.dbtype
==
DatabaseColumnType
::
Bool
{
result
.insert
(
column
.name
.to_string
());
}
}
result
.insert
(
"deleted"
.to_string
());
result
columns
.map
(|
c
|
(
c
.name
.to_string
(),
c
.dbtype
))
.collect
()
};
}
const
MANDATORY_ITEMS_FIELDS
:
&
[
&
str
]
=
&
[
"uid"
,
"_type"
,
"dateCreated"
,
"dateModified"
,
"deleted"
,
"version"
,
];
pub
const
AUTOGENERATED_SCHEMA
:
&
[
u8
]
=
include_bytes!
(
"../res/autogenerated_database_schema.json"
);
pub
fn
migrate
(
sqlite
:
&
Pool
<
SqliteConnectionManager
>
)
{
let
conn
=
sqlite
.get
()
.expect
(
"Failed to aquire SQLite connection during db initialization"
);
.expect
(
"Failed to a
c
quire SQLite connection during db initialization"
);
info!
(
"Initializing database schema (additional columns)"
);
let
parsed_schema
:
DatabaseSchema
=
serde_json
::
from_slice
(
AUTOGENERATED_SCHEMA
)
.expect
(
"Failed to parse autogenerated_database_schema to JSON"
);
...
...
@@ -199,12 +237,3 @@ where
}
map
}
const
MANDATORY_ITEMS_FIELDS
:
&
[
&
str
]
=
&
[
"uid"
,
"_type"
,
"dateCreated"
,
"dateModified"
,
"deleted"
,
"version"
,
];
This diff is collapsed.
Click to expand it.
src/internal_api.rs
+
1
-
1
View file @
0357587d
...
...
@@ -89,7 +89,7 @@ fn write_sql_body(sql: &mut String, keys: &[&String], separator: &str) {
fn
execute_sql
(
tx
:
&
Transaction
,
sql
:
&
str
,
fields
:
&
HashMap
<
String
,
Value
>
)
->
Result
<
()
>
{
let
mut
sql_params
=
Vec
::
new
();
for
(
key
,
value
)
in
fields
{
sql_params
.push
((
format!
(
":{}"
,
key
),
json_value_to_sqlite
(
value
)
?
));
sql_params
.push
((
format!
(
":{}"
,
key
),
json_value_to_sqlite
(
value
,
key
)
?
));
}
let
sql_params
:
Vec
<
_
>
=
sql_params
.iter
()
...
...
This diff is collapsed.
Click to expand it.
src/sql_converters.rs
+
63
-
20
View file @
0357587d
use
crate
::
database_migrate_schema
;
use
crate
::
error
::
Error
;
use
database_migrate_schema
::
ALL_COLUMN_TYPES
;
use
database_migrate_schema
::
BOOL_COLUMNS
;
use
database_migrate_schema
::
DATE_TIME_COLUMNS
;
use
database_migrate_schema
::
INTEGER_COLUMNS
;
use
database_migrate_schema
::
REAL_COLUMNS
;
use
database_migrate_schema
::
TEXT_COLUMNS
;
use
lazy_static
::
lazy_static
;
use
log
::
warn
;
use
regex
::
Regex
;
use
rusqlite
::
types
::
ToSqlOutput
;
use
rusqlite
::
types
::
ValueRef
;
...
...
@@ -59,14 +66,13 @@ pub fn fields_mapping_to_owned_sql_params(
fields_map
:
&
Map
<
String
,
serde_json
::
Value
>
,
)
->
crate
::
error
::
Result
<
Vec
<
(
String
,
ToSqlOutput
)
>>
{
let
mut
sql_params
=
Vec
::
new
();
for
(
field
,
value
)
in
fields_map
{
match
value
{
Value
::
Array
(
_
)
=>
continue
,
Value
::
Object
(
_
)
=>
continue
,
_
=>
(),
for
(
key
,
value
)
in
fields_map
{
if
value
.is_array
()
||
value
.is_object
()
{
continue
;
};
let
field
=
format!
(
":{}"
,
field
);
sql_params
.push
((
field
,
json_value_to_sqlite
(
value
)
?
));
let
value
=
json_value_to_sqlite
(
value
,
key
)
?
;
let
key
=
format!
(
":{}"
,
key
);
sql_params
.push
((
key
,
value
));
}
Ok
(
sql_params
)
}
...
...
@@ -80,15 +86,42 @@ pub fn borrow_sql_params<'a>(
.collect
()
}
pub
fn
json_value_to_sqlite
(
json
:
&
Value
)
->
crate
::
error
::
Result
<
ToSqlOutput
<
'_
>>
{
pub
fn
json_value_to_sqlite
<
'a
>
(
json
:
&
'a
Value
,
column
:
&
str
,
)
->
crate
::
error
::
Result
<
ToSqlOutput
<
'a
>>
{
match
json
{
Value
::
Null
=>
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Null
)),
Value
::
String
(
s
)
=>
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Text
(
s
.as_bytes
()))),
Value
::
Number
(
n
)
=>
{
Value
::
String
(
s
)
if
TEXT_COLUMNS
.contains
(
column
)
=>
{
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Text
(
s
.as_bytes
())))
}
Value
::
Number
(
n
)
if
INTEGER_COLUMNS
.contains
(
column
)
=>
{
if
let
Some
(
int
)
=
n
.as_i64
()
{
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Integer
(
int
)))
}
else
{
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
msg
:
format!
(
"Failed to parse JSON number {} to i64 ({})"
,
n
,
column
),
})
}
}
Value
::
Number
(
n
)
if
REAL_COLUMNS
.contains
(
column
)
=>
{
if
let
Some
(
int
)
=
n
.as_f64
()
{
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Real
(
int
)))
}
else
{
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
msg
:
format!
(
"Failed to parse JSON number {} to f64 ({})"
,
n
,
column
),
})
}
}
Value
::
Bool
(
b
)
if
BOOL_COLUMNS
.contains
(
column
)
=>
Ok
((
if
*
b
{
1
}
else
{
0
})
.into
()),
Value
::
Number
(
n
)
if
DATE_TIME_COLUMNS
.contains
(
column
)
=>
{
if
let
Some
(
int
)
=
n
.as_i64
()
{
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Integer
(
int
)))
}
else
if
let
Some
(
float
)
=
n
.as_f64
()
{
Ok
(
ToSqlOutput
::
Borrowed
(
ValueRef
::
Real
(
float
)))
warn!
(
"Using float-to-integer conversion property {}, value {}. This might not be supported in the future, please use a compatible DateTime format https://gitlab.memri.io/memri/pod#understanding-the-schema"
,
float
,
column
);
Ok
((
float
.round
()
as
i64
)
.into
())
}
else
{
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
...
...
@@ -96,15 +129,25 @@ pub fn json_value_to_sqlite(json: &Value) -> crate::error::Result<ToSqlOutput<'_
})
}
}
Value
::
Bool
(
b
)
=>
Ok
((
if
*
b
{
1
}
else
{
0
})
.into
()),
Value
::
Array
(
arr
)
=>
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
msg
:
format!
(
"Cannot convert JSON array to an SQL parameter: {:?}"
,
arr
),
}),
Value
::
Object
(
obj
)
=>
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
msg
:
format!
(
"Cannot convert JSON object to an SQL parameter: {:?}"
,
obj
),
}),
_
=>
{
if
let
Some
(
dbtype
)
=
ALL_COLUMN_TYPES
.get
(
column
)
{
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
msg
:
format!
(
"Failed to parse json value {} to {:?} ({})"
,
json
,
dbtype
,
column
),
})
}
else
{
Err
(
Error
{
code
:
StatusCode
::
BAD_REQUEST
,
msg
:
format!
(
"Failed to insert json value {} to property {}, reason: not defined in schema"
,
json
,
column
),
})
}
}
}
}
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
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!
Cancel
Please
register
or
sign in
to comment
Menu
Explore
Projects
Groups
Snippets