[Infer] Refactor sqlite-related code into its own module

Reviewed By: jvillard

Differential Revision: D6186753

fbshipit-source-id: 584e22a
master
Martino Luca 7 years ago committed by Facebook Github Bot
parent c98570f899
commit 259beab26a

@ -65,7 +65,7 @@ let get_replace_statement =
in several files *) in several files *)
(* TRICK: older versions of sqlite (prior to version 3.15.0 (2016-10-14)) do not support row (* TRICK: older versions of sqlite (prior to version 3.15.0 (2016-10-14)) do not support row
values so the lexicographic ordering for (:akind, :sfile) is done by hand *) values so the lexicographic ordering for (:akind, :sfile) is done by hand *)
ResultsDir.register_statement ResultsDatabase.register_statement
{| {|
INSERT OR REPLACE INTO attributes INSERT OR REPLACE INTO attributes
SELECT :pname, :akind, :sfile, :pattr SELECT :pname, :akind, :sfile, :pattr
@ -94,7 +94,7 @@ let replace pname_blob akind loc_file attr_blob =
let get_find_more_defined_statement = let get_find_more_defined_statement =
ResultsDir.register_statement ResultsDatabase.register_statement
{| {|
SELECT attr_kind SELECT attr_kind
FROM attributes FROM attributes
@ -114,11 +114,11 @@ let should_try_to_update pname_blob akind =
let get_select_statement = let get_select_statement =
ResultsDir.register_statement "SELECT proc_attributes FROM attributes WHERE proc_name = :k" ResultsDatabase.register_statement "SELECT proc_attributes FROM attributes WHERE proc_name = :k"
let get_select_defined_statement = let get_select_defined_statement =
ResultsDir.register_statement ResultsDatabase.register_statement
"SELECT proc_attributes FROM attributes WHERE proc_name = :k AND attr_kind = %Ld" "SELECT proc_attributes FROM attributes WHERE proc_name = :k AND attr_kind = %Ld"
(int64_of_attributes_kind ProcDefined) (int64_of_attributes_kind ProcDefined)

@ -44,7 +44,7 @@ let run t =
let fork_protect ~f x = let fork_protect ~f x =
L.reset_formatters () ; L.reset_formatters () ;
ResultsDir.new_database_connection () ; ResultsDatabase.new_database_connection () ;
f x f x

@ -15,7 +15,7 @@ let merge_attributes_table ~db_file =
cases where a proc_name is present in both the sub-table and the main one (main_attr_kind != cases where a proc_name is present in both the sub-table and the main one (main_attr_kind !=
NULL). All the rows that pass this filter are inserted/updated into the main table. *) NULL). All the rows that pass this filter are inserted/updated into the main table. *)
let copy_stmt = let copy_stmt =
Sqlite3.prepare (ResultsDir.get_database ()) Sqlite3.prepare (ResultsDatabase.get_database ())
{| {|
INSERT OR REPLACE INTO attributes INSERT OR REPLACE INTO attributes
SELECT proc_name, attr_kind, source_file, proc_attributes SELECT proc_name, attr_kind, source_file, proc_attributes
@ -40,7 +40,7 @@ WHERE
let merge ~db_file = let merge ~db_file =
(* no need to wrap all the individual table merges in a single transaction (to batch writes) (* no need to wrap all the individual table merges in a single transaction (to batch writes)
because we open the table with synchronous=OFF *) because we open the table with synchronous=OFF *)
let main_db = ResultsDir.get_database () in let main_db = ResultsDatabase.get_database () in
SqliteUtils.check_sqlite_error ~fatal:true SqliteUtils.check_sqlite_error ~fatal:true
~log:(Printf.sprintf "attaching database '%s'" db_file) ~log:(Printf.sprintf "attaching database '%s'" db_file)
(Sqlite3.exec main_db (Printf.sprintf "ATTACH '%s' AS attached" db_file)) ; (Sqlite3.exec main_db (Printf.sprintf "ATTACH '%s' AS attached" db_file)) ;
@ -60,7 +60,7 @@ let merge_buck_flavors_results infer_deps_file =
Filename.dirname (Config.project_root ^/ "buck-out") ^/ target_results_dir Filename.dirname (Config.project_root ^/ "buck-out") ^/ target_results_dir
else target_results_dir else target_results_dir
in in
merge ~db_file:(infer_out_src ^/ ResultsDir.database_filename) merge ~db_file:(infer_out_src ^/ ResultsDatabase.database_filename)
| _ -> | _ ->
assert false assert false
in in

@ -0,0 +1,115 @@
(*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
open! IStd
open! PVariant
module L = Logging
let database : Sqlite3.db option ref = ref None
let database_filename = "results.db"
let database_fullpath = Config.results_dir ^/ database_filename
let create_attributes_table db =
(* it would be nice to use "WITHOUT ROWID" here but ancient versions of sqlite do not support
it *)
SqliteUtils.exec db ~log:"initializing results DB"
~stmt:
{|
CREATE TABLE IF NOT EXISTS attributes
( proc_name TEXT PRIMARY KEY
, attr_kind INTEGER NOT NULL
, source_file TEXT NOT NULL
, proc_attributes BLOB NOT NULL )|}
let create_db () =
let temp_db = Filename.temp_file ~in_dir:Config.results_dir database_filename ".tmp" in
let db = Sqlite3.db_open ~mutex:`FULL temp_db in
create_attributes_table db ;
(* This should be the default but better be sure, otherwise we cannot access the database concurrently. This has to happen before setting WAL mode. *)
SqliteUtils.exec db ~log:"locking mode=NORMAL" ~stmt:"PRAGMA locking_mode=NORMAL" ;
(* Write-ahead log is much faster than other journalling modes. *)
SqliteUtils.exec db ~log:"journal_mode=WAL" ~stmt:"PRAGMA journal_mode=WAL" ;
SqliteUtils.db_close db ;
try Sys.rename temp_db database_fullpath
with Sys_error _ -> (* lost the race, doesn't matter *) ()
let new_db_callbacks = ref []
let on_new_database_connection ~f = new_db_callbacks := f :: !new_db_callbacks
let close_db_callbacks = ref []
let on_close_database ~f = close_db_callbacks := f :: !close_db_callbacks
let get_database () = Option.value_exn !database
let reset_attributes_table () =
let db = get_database () in
SqliteUtils.exec db ~log:"drop attributes table" ~stmt:"DROP TABLE attributes" ;
create_attributes_table db
let db_canonicalize () =
let db = get_database () in
SqliteUtils.exec db ~log:"running VACUUM" ~stmt:"VACUUM"
let register_statement stmt_fmt =
let k stmt0 =
let stmt_ref = ref None in
let new_statement db =
let stmt =
try Sqlite3.prepare db stmt0
with Sqlite3.Error error ->
L.die InternalError "Could not prepare the following statement:@\n%s@\nReason: %s" stmt0
error
in
on_close_database ~f:(fun _ -> SqliteUtils.finalize ~log:"db close callback" stmt) ;
stmt_ref := Some stmt
in
on_new_database_connection ~f:new_statement ;
fun () ->
match !stmt_ref with
| None ->
L.(die InternalError) "database not initialized"
| Some stmt ->
Sqlite3.reset stmt |> SqliteUtils.check_sqlite_error ~log:"reset prepared statement" ;
Sqlite3.clear_bindings stmt
|> SqliteUtils.check_sqlite_error ~log:"clear bindings of prepared statement" ;
stmt
in
Printf.ksprintf k stmt_fmt
let do_db_close db =
List.iter ~f:(fun callback -> callback db) !close_db_callbacks ;
close_db_callbacks := [] ;
SqliteUtils.db_close db
let db_close () =
Option.iter !database ~f:do_db_close ;
database := None
let new_database_connection () =
db_close () ;
let db = Sqlite3.db_open ~mode:`NO_CREATE ~cache:`PRIVATE ~mutex:`FULL database_fullpath in
Sqlite3.busy_timeout db 10_000 ;
(* Higher level of "synchronous" are only useful to guarantee that the db will not be corrupted if the machine crashes for some reason before the data has been actually written to disk. We do not need this kind of guarantee for infer results as one can always rerun infer if interrupted. *)
SqliteUtils.exec db ~log:"synchronous=OFF" ~stmt:"PRAGMA synchronous=OFF" ;
database := Some db ;
List.iter ~f:(fun callback -> callback db) !new_db_callbacks
let () = Epilogues.register "closing results database" ~f:db_close

@ -0,0 +1,41 @@
(*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
val database_filename : string
(** the relative path to the database from the results directory *)
val database_fullpath : string
(** the absolute path to the database file *)
val get_database : unit -> Sqlite3.db
(** The results database. You should always use this function to access the database, as the connection to it may change during the execution (see [new_database_connection]). *)
val reset_attributes_table : unit -> unit
(** zero out the attributes table *)
val new_database_connection : unit -> unit
(** Closes the previous connection to the database (if any), and opens a new one. Needed after calls to fork(2). *)
val db_canonicalize : unit -> unit
(** put the database on disk in deterministic form *)
val db_close : unit -> unit
(** close the current connection to the database *)
val create_db : unit -> unit
(** create the database file and initialize all the necessary tables *)
val register_statement : ('a, unit, string, unit -> Sqlite3.stmt) Base.format4 -> 'a
(** Return a function unit -> Sqlite3.stmt that can be called (once the DB has been initialized) to
get the prepared statement corresponding to the current DB connection. Use this to prepare
statements only once per DB connection.
In particular, clients of this need not worry about calling [Sqlite3.finalize] on the returned
statement, or about generating new statements when the connection to the DB changes: this is all
handled internally. *)

@ -10,12 +10,6 @@ open! IStd
open! PVariant open! PVariant
module L = Logging module L = Logging
let database : Sqlite3.db option ref = ref None
let database_filename = "results.db"
let database_fullpath = Config.results_dir ^/ database_filename
let results_dir_dir_markers = let results_dir_dir_markers =
List.map ~f:(Filename.concat Config.results_dir) [Config.captured_dir_name; Config.specs_dir_name] List.map ~f:(Filename.concat Config.results_dir) [Config.captured_dir_name; Config.specs_dir_name]
@ -28,9 +22,9 @@ let is_results_dir ~check_correct_version () =
|| ||
(not_found := d ^ "/" ; (not_found := d ^ "/" ;
false) ) false) )
&& ( not check_correct_version || Sys.is_file database_fullpath = `Yes && ( not check_correct_version || Sys.is_file ResultsDatabase.database_fullpath = `Yes
|| ||
(not_found := database_fullpath ; (not_found := ResultsDatabase.database_fullpath ;
false) ) false) )
in in
Result.ok_if_true has_all_markers ~error:(Printf.sprintf "'%s' not found" !not_found) Result.ok_if_true has_all_markers ~error:(Printf.sprintf "'%s' not found" !not_found)
@ -48,68 +42,11 @@ let remove_results_dir () =
Utils.rmtree Config.results_dir ) Utils.rmtree Config.results_dir )
let create_attributes_table db =
(* it would be nice to use "WITHOUT ROWID" here but ancient versions of sqlite do not support
it *)
SqliteUtils.exec db ~log:"initializing results DB"
~stmt:
{|
CREATE TABLE IF NOT EXISTS attributes
( proc_name TEXT PRIMARY KEY
, attr_kind INTEGER NOT NULL
, source_file TEXT NOT NULL
, proc_attributes BLOB NOT NULL )|}
let create_db () =
let temp_db = Filename.temp_file ~in_dir:Config.results_dir database_filename ".tmp" in
let db = Sqlite3.db_open ~mutex:`FULL temp_db in
create_attributes_table db ;
(* This should be the default but better be sure, otherwise we cannot access the database concurrently. This has to happen before setting WAL mode. *)
SqliteUtils.exec db ~log:"locking mode=NORMAL" ~stmt:"PRAGMA locking_mode=NORMAL" ;
(* Write-ahead log is much faster than other journalling modes. *)
SqliteUtils.exec db ~log:"journal_mode=WAL" ~stmt:"PRAGMA journal_mode=WAL" ;
SqliteUtils.db_close db ;
try Sys.rename temp_db database_fullpath
with Sys_error _ -> (* lost the race, doesn't matter *) ()
let new_db_callbacks = ref []
let on_new_database_connection ~f = new_db_callbacks := f :: !new_db_callbacks
let close_db_callbacks = ref []
let on_close_database ~f = close_db_callbacks := f :: !close_db_callbacks
let do_db_close db =
List.iter ~f:(fun callback -> callback db) !close_db_callbacks ;
close_db_callbacks := [] ;
SqliteUtils.db_close db
let db_close () =
Option.iter !database ~f:do_db_close ;
database := None
let new_database_connection () =
db_close () ;
let db = Sqlite3.db_open ~mode:`NO_CREATE ~cache:`PRIVATE ~mutex:`FULL database_fullpath in
Sqlite3.busy_timeout db 10_000 ;
(* Higher level of "synchronous" are only useful to guarantee that the db will not be corrupted if the machine crashes for some reason before the data has been actually written to disk. We do not need this kind of guarantee for infer results as one can always rerun infer if interrupted. *)
SqliteUtils.exec db ~log:"synchronous=OFF" ~stmt:"PRAGMA synchronous=OFF" ;
database := Some db ;
List.iter ~f:(fun callback -> callback db) !new_db_callbacks
let () = Epilogues.register "closing results database" ~f:db_close
let create_results_dir () = let create_results_dir () =
Unix.mkdir_p Config.results_dir ; Unix.mkdir_p Config.results_dir ;
L.setup_log_file () ; L.setup_log_file () ;
if Sys.file_exists database_fullpath <> `Yes then create_db () ; if Sys.is_file ResultsDatabase.database_fullpath <> `Yes then ResultsDatabase.create_db () ;
new_database_connection () ; ResultsDatabase.new_database_connection () ;
List.iter ~f:Unix.mkdir_p results_dir_dir_markers List.iter ~f:Unix.mkdir_p results_dir_dir_markers
@ -118,55 +55,14 @@ let assert_results_dir advice =
L.(die UserError) L.(die UserError)
"ERROR: No results directory at '%s': %s@\nERROR: %s@." Config.results_dir err advice ) ; "ERROR: No results directory at '%s': %s@\nERROR: %s@." Config.results_dir err advice ) ;
L.setup_log_file () ; L.setup_log_file () ;
new_database_connection () ResultsDatabase.new_database_connection ()
let get_database () = Option.value_exn !database
let reset_attributes_table () =
let db = get_database () in
SqliteUtils.exec db ~log:"drop attributes table" ~stmt:"DROP TABLE attributes" ;
create_attributes_table db
let delete_capture_and_analysis_data () = let delete_capture_and_analysis_data () =
reset_attributes_table () ; ResultsDatabase.reset_attributes_table () ;
let dirs_to_delete = let dirs_to_delete =
List.map ~f:(Filename.concat Config.results_dir) Config.([captured_dir_name; specs_dir_name]) List.map ~f:(Filename.concat Config.results_dir) Config.([captured_dir_name; specs_dir_name])
in in
List.iter ~f:Utils.rmtree dirs_to_delete ; List.iter ~f:Utils.rmtree dirs_to_delete ;
List.iter ~f:Unix.mkdir_p dirs_to_delete ; List.iter ~f:Unix.mkdir_p dirs_to_delete ;
() ()
let db_canonicalize () =
let db = get_database () in
SqliteUtils.exec db ~log:"running VACUUM" ~stmt:"VACUUM"
let register_statement stmt_fmt =
let k stmt0 =
let stmt_ref = ref None in
let new_statement db =
let stmt =
try Sqlite3.prepare db stmt0
with Sqlite3.Error error ->
L.die InternalError "Could not prepare the following statement:@\n%s@\nReason: %s" stmt0
error
in
on_close_database ~f:(fun _ -> SqliteUtils.finalize ~log:"db close callback" stmt) ;
stmt_ref := Some stmt
in
on_new_database_connection ~f:new_statement ;
fun () ->
match !stmt_ref with
| None ->
L.(die InternalError) "database not initialized"
| Some stmt ->
Sqlite3.reset stmt |> SqliteUtils.check_sqlite_error ~log:"reset prepared statement" ;
Sqlite3.clear_bindings stmt
|> SqliteUtils.check_sqlite_error ~log:"clear bindings of prepared statement" ;
stmt
in
Printf.ksprintf k stmt_fmt

@ -9,15 +9,6 @@
open! IStd open! IStd
val database_filename : string
(** the relative path to the database from the results directory *)
val get_database : unit -> Sqlite3.db
(** The results database. You should always use this function to access the database, as the connection to it may change during the execution (see [new_database_connection]). *)
val reset_attributes_table : unit -> unit
(** zero out the attributes table *)
val assert_results_dir : string -> unit val assert_results_dir : string -> unit
(** Check that the results dir exists and sets up logging, the database, etc. *) (** Check that the results dir exists and sets up logging, the database, etc. *)
@ -27,23 +18,5 @@ val remove_results_dir : unit -> unit
val create_results_dir : unit -> unit val create_results_dir : unit -> unit
(** Create the results dir and sets up logging, the database, etc. *) (** Create the results dir and sets up logging, the database, etc. *)
val new_database_connection : unit -> unit
(** Closes the previous connection to the database (if any), and opens a new one. Needed after calls to fork(2). *)
val delete_capture_and_analysis_data : unit -> unit val delete_capture_and_analysis_data : unit -> unit
(** delete all results from the capture and the analysis *) (** delete all results from the capture and the analysis *)
val db_canonicalize : unit -> unit
(** put the database on disk in deterministic form *)
val db_close : unit -> unit
(** close the current connection to the database *)
val register_statement : ('a, unit, string, unit -> Sqlite3.stmt) Base.format4 -> 'a
(** Return a function unit -> Sqlite3.stmt that can be called (once the DB has been initialized) to
get the prepared statement corresponding to the current DB connection. Use this to prepare
statements only once per DB connection.
In particular, clients of this need not worry about calling [Sqlite3.finalize] on the returned
statement, or about generating new statements when the connection to the DB changes: this is all
handled internally. *)

@ -67,10 +67,10 @@ let clean_compilation_command mode =
let clean_results_dir () = let clean_results_dir () =
if not Config.flavors then if not Config.flavors then
(* we do not need to keep the capture data in Buck/Java mode *) (* we do not need to keep the capture data in Buck/Java mode *)
ResultsDir.reset_attributes_table () ; ResultsDatabase.reset_attributes_table () ;
ResultsDir.db_canonicalize () ; ResultsDatabase.db_canonicalize () ;
(* make sure we are done with the database *) (* make sure we are done with the database *)
ResultsDir.db_close () ; ResultsDatabase.db_close () ;
(* In Buck flavors mode we keep all capture data, but in Java mode we keep only the tenv *) (* In Buck flavors mode we keep all capture data, but in Java mode we keep only the tenv *)
let should_delete_dir = let should_delete_dir =
let dirs_to_delete = let dirs_to_delete =
@ -87,8 +87,8 @@ let clean_results_dir () =
let files_to_delete = let files_to_delete =
[ Config.log_file [ Config.log_file
; (* some versions of sqlite do not clean up after themselves *) ; (* some versions of sqlite do not clean up after themselves *)
ResultsDir.database_filename ^ "-shm" ResultsDatabase.database_filename ^ "-shm"
; ResultsDir.database_filename ^ "-wal" ] ; ResultsDatabase.database_filename ^ "-wal" ]
in in
let suffixes_to_delete = let suffixes_to_delete =
".txt" :: ".csv" :: ".json" :: (if Config.flavors then [] else [".cfg"; ".cg"]) ".txt" :: ".csv" :: ".json" :: (if Config.flavors then [] else [".cfg"; ".cg"])
@ -579,4 +579,3 @@ let read_config_changed_files () =
| Error error -> | Error error ->
L.external_error "Error reading the changed files index '%s': %s@." index error ; L.external_error "Error reading the changed files index '%s': %s@." index error ;
None None

Loading…
Cancel
Save