-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: ppx #6
Closed
Closed
WIP: ppx #6
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
open Ast_helper | ||
open Asttypes | ||
open Parsetree | ||
|
||
module AC = Ast_convenience | ||
|
||
let new_id = | ||
let n = ref 0 in | ||
fun () -> | ||
incr n; | ||
Printf.sprintf "__ppx_sql_%d" !n | ||
|
||
let gen_stmt ~cacheable sql inp = | ||
let mkapply fn args = AC.app (AC.evar fn) args in | ||
|
||
let k = new_id () in | ||
let st = new_id () in | ||
let id = | ||
let signature = | ||
Printf.sprintf "%d-%f-%d-%S" | ||
Unix.(getpid ()) (Unix.gettimeofday ()) (Random.int 0x3FFFFFF) sql | ||
in Digest.to_hex (Digest.string signature) in | ||
let stmt_id = | ||
if cacheable | ||
then [%expr Some [%e AC.str id ]] | ||
else [%expr None] in | ||
let exp = List.fold_right (fun elem dir -> | ||
let typ = Sqlexpr_parser.in_type2str elem in | ||
[%expr [%e mkapply typ [dir]]]) | ||
inp | ||
[%expr [%e AC.evar k]] in | ||
let dir = [%expr fun [%p AC.pvar k] -> fun [%p AC.pvar st] -> | ||
let open Sqlexpr.Directives in [%e exp] [%e AC.evar st] | ||
] in | ||
[%expr { | ||
Sqlexpr.sql_statement = [%e AC.str sql]; | ||
stmt_id = [%e stmt_id]; | ||
directive = [%e dir]; | ||
}] | ||
|
||
let gen_expr ~cacheable sql inp outp = | ||
let stmt = gen_stmt ~cacheable sql inp in | ||
let id = new_id () in | ||
let conv s = Longident.(Ldot (Ldot (Lident "Sqlexpr", "Conversion"), s)) in | ||
let conv_exprs = List.mapi (fun i elem -> | ||
let txt = conv (Sqlexpr_parser.out_type2str elem) in | ||
let fn = Exp.ident {txt; loc=(!default_loc)} in | ||
let args = [%expr Array.get [%e AC.evar id] [%e AC.int i]] in | ||
AC.app fn [args]) outp in | ||
let tuple_func = | ||
let e = match conv_exprs with | ||
[] -> assert false | ||
| [x] -> x | ||
| hd::tl -> Exp.tuple conv_exprs in | ||
[%expr fun [%p AC.pvar id] -> [%e e]] in | ||
[%expr { | ||
Sqlexpr.statement = [%e stmt]; | ||
get_data = ([%e AC.int (List.length outp)], [%e tuple_func]); | ||
}] | ||
|
||
let stmts = ref [] | ||
let init_stmts = ref [] | ||
|
||
let gen_sql ?(init=false) ?(cacheable=false) str = | ||
let (sql, inp, outp) = Sqlexpr_parser.parse str in | ||
|
||
(* accumulate statements *) | ||
if init | ||
then init_stmts := sql :: !init_stmts | ||
else stmts := sql :: !stmts; | ||
|
||
if [] = outp | ||
then gen_stmt ~cacheable sql inp | ||
else gen_expr ~cacheable sql inp outp | ||
|
||
let sqlcheck_sqlite () = | ||
let mkstr s = Exp.constant (Const_string (s, None)) in | ||
let statement_check = [%expr | ||
try ignore(Sqlite3.prepare db stmt) | ||
with Sqlite3.Error s -> | ||
ret := false; | ||
Format.fprintf fmt "Error in statement %S: %s\n" stmt s | ||
] in | ||
let stmt_expr_f acc elem = [%expr [%e mkstr elem] :: [%e acc]] in | ||
let stmt_exprs = List.fold_left stmt_expr_f [%expr []] !stmts in | ||
let init_exprs = List.fold_left stmt_expr_f [%expr []] !init_stmts in | ||
let check_db_expr = [%expr fun db fmt -> | ||
let ret = ref true in | ||
List.iter (fun stmt -> [%e statement_check]) [%e stmt_exprs]; | ||
!ret | ||
] in | ||
let init_db_expr = [%expr fun db fmt -> | ||
let ret = ref true in | ||
List.iter (fun stmt -> match Sqlite3.exec db stmt with | ||
| Sqlite3.Rc.OK -> () | ||
| rc -> begin | ||
ret := false; | ||
Format.fprintf fmt "Error in init. SQL statement (%s)@ %S@\n" | ||
(Sqlite3.errmsg db) stmt | ||
end) [%e init_exprs]; | ||
!ret | ||
] in | ||
let in_mem_check_expr = [%expr fun fmt -> | ||
let db = Sqlite3.db_open ":memory:" in | ||
init_db db fmt && check_db db fmt | ||
] in | ||
[%expr | ||
let init_db = [%e init_db_expr] in | ||
let check_db = [%e check_db_expr] in | ||
let in_mem_check = [%e in_mem_check_expr] in | ||
(init_db, check_db, in_mem_check) | ||
] | ||
|
||
let call fn loc = function | ||
| PStr [ {pstr_desc = Pstr_eval ( | ||
{ pexp_desc = Pexp_constant(Const_string(sym, _))}, _)} ] -> | ||
with_default_loc loc (fun () -> fn sym) | ||
| _ -> raise (Location.Error(Location.error ~loc ( | ||
"sqlexpr extension accepts a string"))) | ||
|
||
let call_sqlcheck loc = function | ||
| PStr [ {pstr_desc = Pstr_eval ({ pexp_desc = | ||
Pexp_constant(Const_string("sqlite", None))}, _)}] -> | ||
with_default_loc loc sqlcheck_sqlite | ||
| _ -> raise (Location.Error(Location.error ~loc ( | ||
"sqlcheck extension accepts \"sqlite\""))) | ||
|
||
let new_mapper argv = Ast_mapper.({ | ||
default_mapper with | ||
expr = fun mapper expr -> | ||
match expr with | ||
(* is this an extension node? *) | ||
| {pexp_desc = Pexp_extension ({txt = "sql"; loc}, pstr)} -> | ||
call gen_sql loc pstr | ||
| {pexp_desc = Pexp_extension ({txt = "sqlc"; loc}, pstr)} -> | ||
call (gen_sql ~cacheable:true) loc pstr | ||
| {pexp_desc = Pexp_extension ({txt = "sqlinit"; loc}, pstr)} -> | ||
call (gen_sql ~init:true) loc pstr | ||
| {pexp_desc = Pexp_extension ({txt = "sqlcheck"; loc}, pstr)} -> | ||
call_sqlcheck loc pstr | ||
(* Delegate to the default mapper *) | ||
| x -> default_mapper.expr mapper x; | ||
}) | ||
|
||
let () = | ||
Random.self_init (); | ||
Ast_mapper.register "sqlexpr" new_mapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
type typ = Int | Int32 | Int64 | Float | Text | Blob | Bool | Any | ||
type input = typ * bool | ||
type output = string * typ * bool | ||
|
||
let str2typ = function | ||
| "d" -> Int | ||
| "l" -> Int32 | ||
| "L" -> Int64 | ||
| "f" -> Float | ||
| "s" -> Text | ||
| "S" -> Blob | ||
| "b" -> Bool | ||
| "a" -> Any | ||
| _ -> failwith "Invalid type" | ||
|
||
let typ2str = function | ||
| Int -> "int" | ||
| Int32 -> "int32" | ||
| Int64 -> "int64" | ||
| Float -> "float" | ||
| Text -> "text" | ||
| Blob -> "blob" | ||
| Bool -> "bool" | ||
| Any -> "any" | ||
|
||
let in_type2str ((typ, optional) : input) = | ||
let typ = typ2str typ in | ||
if optional then "maybe_" ^ typ else typ | ||
|
||
let out_type2str ((_, typ, optional) : output) = | ||
in_type2str (typ, optional) | ||
|
||
let parse str = | ||
(* grievous hack to escape everything within quotes *) | ||
(* what about ignoring \' or \" ? " *) | ||
(* manually escape "" because {| ... |} notation breaks syntax highlighting *) | ||
let escrgx = Re_pcre.regexp "('[^']*')|(\"[^\"]*\")" in | ||
let esc_list = ref [] in | ||
let esc_str = "<SQLEXPR_PRESERVED>" in | ||
let esc_subst substrings = | ||
let mtch = Re.get substrings 0 in | ||
esc_list := mtch :: !esc_list; | ||
esc_str in | ||
|
||
let escaped = Re.replace ~f:esc_subst escrgx str in | ||
esc_list := List.rev !esc_list; | ||
|
||
(* logic to extract inputs and outputs *) | ||
let inrgx = Re_pcre.regexp {|%([dlLfsSba])(\?)?|} in | ||
let outrgx = Re_pcre.regexp {|@([dlLfsSba])(\?)?\{((\w|\.)+)\}|} in | ||
let getin (acc : input list) s = | ||
let groups = Re.get_all s in | ||
let typ = Array.get groups 1 |> str2typ in | ||
let optional = "?" = Array.get groups 2 in | ||
let res = typ, optional in | ||
res::acc in | ||
let getout (acc : output list) s = | ||
let groups = Re.get_all s in | ||
let typ = Array.get groups 1 |> str2typ in | ||
let optional = "?" = Array.get groups 2 in | ||
let name = Array.get groups 3 |> String.trim in | ||
let res = name, typ, optional in | ||
res::acc in | ||
|
||
(* execute extractions *) | ||
let ins = Re.all inrgx escaped |> List.fold_left getin [] |> List.rev in | ||
let outs = Re.all outrgx escaped |> List.fold_left getout [] |> List.rev in | ||
|
||
(* replace input and output params with regular SQL *) | ||
let in_subst substrs = "?" in | ||
|
||
let rep_count_out = ref 0 in | ||
let out_subst substrs = | ||
let (name, _,_) = List.nth outs !rep_count_out in | ||
incr rep_count_out; | ||
name in | ||
|
||
(* now restore the escaped strings *) | ||
let rep_esc_count = ref 0 in | ||
let unesc_subst substrs = | ||
let restore = List.nth !esc_list !rep_esc_count in | ||
incr rep_esc_count; | ||
restore in | ||
|
||
(* generate final sql *) | ||
let sql = | ||
Re.replace ~f:out_subst outrgx escaped | ||
|> Re.replace ~f:in_subst inrgx | ||
|> Re.replace ~f:unesc_subst (Re_pcre.regexp esc_str) in | ||
|
||
(* final return *) | ||
(sql, ins, outs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
open OUnit | ||
|
||
module Sqlexpr = Sqlexpr_sqlite.Make(Sqlexpr_concurrency.Id) | ||
module S = Sqlexpr | ||
|
||
let ae = assert_equal ~printer:(fun x -> x) | ||
|
||
let pqi (d : ('a , 'b) S.statement) = d.S.sql_statement | ||
let pq (d : ('a, 'b, 'c) S.expression) = pqi d.S.statement | ||
|
||
let test_sql _ = | ||
|
||
let s = pqi [%sql "insert into values(%s{foo}, %s{bar})"] in | ||
ae "insert into values(?{foo}, ?{bar})" s; | ||
|
||
let s = pq [%sql "@d{kilroy} was @s{here}"] in | ||
ae "kilroy was here" s; | ||
|
||
(* dots in column names should be okay *) | ||
let s = pq [%sql "select @d{t1.id}, @s{t1.label} from table as t1 ..."] in | ||
ae "select t1.id, t1.label from table as t1 ..." s; | ||
|
||
(* verifies the order of regex substitution. output substitution pass runs | ||
* before the input pass to avoid injecting valid sqlexpr metacharacter '?'. | ||
* For example, given the following string, running the input pass first would | ||
* result in a valid sqlexpr string, leading to an incorrect substitiion in | ||
* the output pass. never mind that immediately adjacent inputs+outputs in | ||
* valid SQL are extremely unlikely... *) | ||
let s = pqi [%sql "@s%d{abc}"] in | ||
ae "@s?{abc}" s; | ||
|
||
(* test invalid expressions and adjacencies *) | ||
let s = pq [%sql "@s@s %d@s{abc}%d@s%d@s%d{def}%d{ghi}@s"] in | ||
ae "@s@s ?abc?@s?@s?{def}?{ghi}@s" s; | ||
|
||
(* column name is not alphanumeric so leave as-is (only dots are allowed) *) | ||
(* also check that whitespace is preserved *) | ||
let s = pqi [%sql "@s{:kilroy} @@was %@{here}"] in | ||
ae "@s{:kilroy} @@was %@{here}" s; | ||
|
||
let s = pqi [%sql "excellent"] in | ||
ae "excellent" s | ||
|
||
|
||
let test_quotes _ = | ||
|
||
(* single quotes *) | ||
let s = pq [%sql "strftime('%s-%d', %s-%d @s{abc}%d{def} '@s{abc}%d{def}')"] in | ||
ae "strftime('%s-%d', ?-? abc?{def} '@s{abc}%d{def}')" s; | ||
|
||
(* double quotes *) | ||
let s = pq [%sql{|strftime("%s-%d", %s-%d @s{abc}%d{def} "@s{abc}%d{def}")|}] in | ||
ae {|strftime("%s-%d", ?-? abc?{def} "@s{abc}%d{def}")|} s; | ||
|
||
(* mixed quotes and nested quotes *) | ||
let s = pq [%sql {|@s{abc}"@s{def}"'@d{ghi}''%f'%f"%S"%S "'@s{jkl}%d'" '"%d'"|}] in | ||
ae {|abc"@s{def}"'@d{ghi}''%f'?"%S"? "'@s{jkl}%d'" '"%d'"|} s; | ||
|
||
(* more nested and unbalanced quotes *) | ||
let s = pqi [%sql {|"'%d'" %d '"%d"' "'%d"'|}] in | ||
ae {|"'%d'" ? '"%d"' "'%d"'|} s | ||
|
||
|
||
let tests = | ||
"ppx_tests">::: [ | ||
"test_sql">::test_sql; | ||
"test_quotes">::test_quotes; | ||
] | ||
|
||
let _ = run_test_tt_main tests |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This (L50) seems to be the culprit of ppx_sqlexpr choking on the likes of
[%sql "SELECT @d{%d}"]
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. Fixed this and all the other remaining issues, sent another PR that goes into your ppx branch: #10