aboutsummaryrefslogblamecommitdiffstats
path: root/bin/server.ml
blob: e66948982e0c9cf69d1be92d9268dc7721c4233d (plain) (tree)
1
2
3
4
5
6
7
8
9




                       



                                                                             
 

                                                                          

















                                                              



                                                           

                                                                      









                                                                                        

                                                                     








                                                                       


                             




                                                     


                                




                                                     

   
                        
                                                                    
                                                                         








                                                                            


                                         

                                                                        



                                               


                                                            
                                                             






                                                                             












                                                                       












                                                                              


                                                                     

























                                                                    

                                                                           







                                          






                                                          





                                              














                                                                                                                                                                                           







                                                                         
                                                                                     


                                                                                













































































                                                                                                            
               








                                                                                     

                                                                                
        






















                                                                                     
open Relational_engine

let default_port = 7777
let default_limit = 50

(* TODO: AbstractValue carries no type tag of its own; we recover the OCaml
   runtime tag here via Obj introspection. This is fragile — it breaks the
   moment a domain stores a boxed integer or a custom block. The right fix is
   a typed value representation in Conventions. *)

(* TODO: Generator.Error is silently treated as end-of-stream here, hiding
   real failures from the client. *)
let materialize_generator gen limit =
  let rec go gen pos acc count =
    if count >= limit then (List.rev acc, true)
    else
      match gen (Some pos) with
      | Generator.Done       -> (List.rev acc, false)
      | Generator.Error _    -> (List.rev acc, false)
      | Generator.Value (t, next) ->
        let mat = match t with
          | Tuple.Materialized m -> Some m
          | Tuple.NonMaterialized _ -> None
        in
        (match mat with
         | None   -> go next (pos + 1) acc count
         | Some m -> go next (pos + 1) (m :: acc) (count + 1))
  in
  go gen 0 [] 0

let materialize_relation storage (rel : Relation.t) limit =
  let gen = Algebra.Memory.to_generator storage rel in
  materialize_generator gen limit

let short_hash (h : string) = String.sub h 0 (min 8 (String.length h))

let tuple_to_sexp (t : Tuple.materialized) =
  let open Sexplib.Sexp in
  List (Tuple.AttributeMap.bindings t.Tuple.attributes
        |> List.map (fun (k, attr) ->
               List [Atom k; Conventions.AbstractValue.sexp_of_t attr.Attribute.value]))

let relation_to_sexp storage (db_hash : string) (rel : Relation.t) limit =
  let open Sexplib.Sexp in
  let schema_sexp =
    List (List.map (fun (a, d) -> List [Atom a; Atom d]) rel.Relation.schema)
  in
  let (tuples, truncated) = materialize_relation storage rel limit in
  let rows_sexp = List (List.map tuple_to_sexp tuples) in
  to_string @@ List [
    Atom "relation";
    List [Atom "name";      Atom rel.Relation.name];
    List [Atom "schema";    schema_sexp];
    List [Atom "rows";      rows_sexp];
    List [Atom "row_count"; Atom (string_of_int (List.length tuples))];
    List [Atom "truncated"; Atom (string_of_bool truncated)];
    List [Atom "db_hash";   Atom (short_hash db_hash)];
  ]

let ok_response db_hash msg =
  let open Sexplib.Sexp in
  to_string @@ List [
    Atom "ok";
    List [Atom "message"; Atom msg];
    List [Atom "db_hash"; Atom (short_hash db_hash)];
  ]

let error_response db_hash msg =
  let open Sexplib.Sexp in
  to_string @@ List [
    Atom "error";
    List [Atom "message"; Atom msg];
    List [Atom "db_hash"; Atom (short_hash db_hash)];
  ]

let manip_err = function
  | Manipulation.RelationNotFound s      -> "RelationNotFound: " ^ s
  | Manipulation.RelationAlreadyExists s -> "RelationAlreadyExists: " ^ s
  | Manipulation.TupleNotFound h         -> "TupleNotFound: " ^ h
  | Manipulation.DuplicateTuple h        -> "DuplicateTuple: " ^ h
  | Manipulation.ConstraintViolation s   -> "ConstraintViolation: " ^ s
  | Manipulation.StorageError s          -> "StorageError: " ^ s

(* TODO: language dispatch by trying parsers in sequence is a code smell —
   a valid DRL expression that happens to also parse as DML would silently
   be treated as DRL. The three sublanguages should share a common envelope,
   e.g. a top-level tag distinguishing (Query ...) from (Exec ...). *)
let execute_command storage db_ref cmd =
  let db = !db_ref in
  let h  = db.Management.Database.hash in
  (* TODO: (schema) is a stopgap. Introspection should be expressible in
     DRL itself via the catalog relations, not special-cased here. *)
  let cmd = match String.trim cmd with
    | "(schema)" -> {|(Base sakura:attribute)|}
    | s -> s
  in
  match Drl.Parser.of_string cmd with
  | Ok query ->
    (match Drl.Executor.Memory.execute storage db query with
     | Ok rel -> relation_to_sexp storage h rel default_limit
     | Error (Drl.Executor.Memory.RelationNotFound s) ->
       error_response h ("RelationNotFound: " ^ s)
     | Error (Drl.Executor.Memory.AlgebraError (Algebra.StorageError s)) ->
       error_response h ("StorageError: " ^ s)
     | Error (Drl.Executor.Memory.AlgebraError (Algebra.GeneratorError s)) ->
       error_response h ("GeneratorError: " ^ s))
  | Error _ ->
    match Dml.Parser.of_string cmd with
    | Ok stmt ->
      (match Dml.Executor.Memory.execute storage db stmt with
       | Ok new_db ->
         db_ref := new_db;
         ok_response new_db.Management.Database.hash "Database updated"
       | Error (Dml.Executor.Memory.ManipulationError me) ->
         error_response h (manip_err me)
       | Error (Dml.Executor.Memory.RelationNotFound s) ->
         error_response h ("RelationNotFound: " ^ s)
       | Error (Dml.Executor.Memory.ParseError s) ->
         error_response h ("ParseError: " ^ s))
    | Error _ ->
      match Dcl.Parser.of_string cmd with
      | Ok stmt ->
        (match Dcl.Executor.Memory.execute storage db stmt with
         | Ok new_db ->
           db_ref := new_db;
           ok_response new_db.Management.Database.hash "Constraint registered"
         | Error (Dcl.Executor.Memory.ManipulationError me) ->
           error_response h (manip_err me)
         | Error (Dcl.Executor.Memory.ConversionError s) ->
           error_response h ("ConversionError: " ^ s))
      | Error _ ->
        error_response h "Parse error: not valid DRL, DML, or DCL"

(* TODO: registration failures are silently ignored; a broken prelude
   relation leaves the catalog in a partially-seeded state with no
   indication to the user. *)
let register_prelude_relations storage db =
  let open Prelude.Standard in
  List.fold_left
    (fun db (rel : Relation.t) ->
      match Manipulation.Memory.create_immutable_relation storage db
              ~name:rel.name
              ~schema:rel.schema
              ~generator:(Option.get rel.generator)
              ~membership_criteria:rel.membership_criteria
              ~cardinality:rel.cardinality
      with
      | Ok (new_db, _) -> new_db
      | Error _        -> db)
    db
    [ less_than_natural
    ; less_than_or_equal_natural
    ; greater_than_natural
    ; greater_than_or_equal_natural
    ; equal_natural
    ; not_equal_natural
    ; plus_natural
    ; times_natural
    ; minus_natural
    ; divide_natural
    ]

(* TODO: single-threaded accept loop — one slow or hung client blocks all
   others. Should use threads or non-blocking I/O. *)
let handle_client storage db_ref fd =
  let ic = Unix.in_channel_of_descr fd in
  let oc = Unix.out_channel_of_descr fd in
  (try
     while true do
       let line = input_line ic in
       let line = String.trim line in
       if line <> "" then begin
         let response =
           try execute_command storage db_ref line
           with e ->
             let db = !db_ref in
             error_response db.Management.Database.hash
               ("Uncaught error: " ^ Printexc.to_string e)
         in
         output_string oc (response ^ "\n");
         flush oc
       end
     done
   with End_of_file | Unix.Unix_error _ -> ())

let depart_emp = [
    "(CreateRelation (name \"Department\") (schema ((dept_id \"integer\") (dept_name \"string\") (location \"string\"))))";
    "(CreateRelation (name \"Employee\") (schema ((emp_id \"integer\") (emp_name \"string\") (salary \"float\") (dept_id \"integer\") (hire_date \"string\"))))";
    "(RegisterConstraint(constraint_name \"salary_positive\")(relation_name \"Employee\")(body (MemberOf(target \"positive_reals\")(binding ((salary (Var \"salary\")))))))";
    "(RegisterConstraint
   (constraint_name \"fk_employee_dept\")
   (relation_name \"Employee\")
   (body (MemberOf
     (target \"Department\")
     (binding ((dept_id (Var \"dept_id\")))))))";
    "(InsertTuple (relation \"Department\") (attributes ((dept_id (Int 1)) (location (Str \"Gifu\")) (dept_name (Str \"Nippon Ichi\")))))";
    "(InsertTuples (relation \"Employee\") (tuples (((emp_id (Int 101)) (emp_name (Str \"Alice Johnson\")) (salary (Float 95000.0)) (dept_id (Int 1)) (hire_date (Str \"2020-01-15\"))))))"
  ]

let _depart_emp () =
  match Management.Physical.Memory.create () with
  | Error _ -> failwith "Failed to create storage"
  | Ok storage ->
    match Manipulation.Memory.create_database storage ~name:"sakura" with
    | Error _ -> failwith "Failed to create initial database"
    | Ok db ->
      let db = register_prelude_relations storage db in
      let db_ref = ref db in
      Printf.printf "Database hash: %s\n%!" (short_hash db.Management.Database.hash);
      List.iter (fun cmd -> print_endline @@ execute_command storage db_ref cmd)
        depart_emp;
      ()

let n_way = [
    "(CreateRelation
  (name \"Building\")
  (schema
    ((building_id \"integer\")
     (building_name \"string\")
     (floors \"integer\"))))";
    "(CreateRelation
     (name \"Room\")
  (schema
    ((room_id \"integer\")
     (building_id \"integer\")
     (floor \"integer\")
     (room_number \"string\"))))";
"(CreateRelation
  (name \"Suite\")
  (schema
    ((suite_id \"integer\")
     (room_id \"integer\")
     (suite_name \"string\")
 (capacity \"integer\"))))";
(* FK: every Room must belong to an existing Building *)
"(RegisterConstraint
  (constraint_name \"fk_room_building\")
  (relation_name \"Room\")
  (body (MemberOf
    (target \"Building\")
    (binding ((building_id (Var \"building_id\")))))))";
(* FK: every Suite must belong to an existing Room *)
"(RegisterConstraint
  (constraint_name \"fk_suite_room\")
  (relation_name \"Suite\")
  (body (MemberOf
    (target \"Room\")
 (binding ((room_id (Var \"room_id\")))))))";
(* A suite is only valid if its room belongs to a building with more than 3 floors *)
"(RegisterConstraint
  (constraint_name \"suite_in_tall_building\")
  (relation_name \"Suite\")
  (body
    (Exists (variable \"r\") (quantifier \"Room\")
      (body
        (Exists (variable \"b\") (quantifier \"Building\")
          (body
            (And
              ((MemberOf (target \"Room\")
                 (binding ((room_id (Var \"room_id\")))))
               (MemberOf (target \"Building\")
                 (binding ((building_id (Var \"r.building_id\")))))
               (MemberOf (target \"greater_than_natural\")
                 (binding ((left (Var \"b.floors\")) (right (Const (Int 3))))))))))))))";
"(InsertTuple
  (relation \"Building\")
  (attributes
    ((building_id (Int 2))
     (building_name (Str \"Tower B\"))
 (floors (Int 2)))))";
    "(InsertTuple
  (relation \"Building\")
  (attributes
    ((building_id (Int 1))
     (building_name (Str \"Tower A\"))
     (floors (Int 10)))))";
"(InsertTuples
  (relation \"Room\")
  (tuples
    (((room_id (Int 101)) (building_id (Int 1)) (floor (Int 1)) (room_number (Str \"1A\")))
     ((room_id (Int 102)) (building_id (Int 1)) (floor (Int 2)) (room_number (Str \"2A\")))
 ((room_id (Int 201)) (building_id (Int 2)) (floor (Int 1)) (room_number (Str \"1B\"))))))";
"(InsertTuples
  (relation \"Suite\")
  (tuples
    (((suite_id (Int 1001)) (room_id (Int 101)) (suite_name (Str \"Presidential\")) (capacity (Int 4)))
     ((suite_id (Int 1002)) (room_id (Int 101)) (suite_name (Str \"Standard\"))     (capacity (Int 2)))
     ((suite_id (Int 1003)) (room_id (Int 102)) (suite_name (Str \"Deluxe\"))       (capacity (Int 3))))))";
  ]

let _n_way () =
  match Management.Physical.Memory.create () with
  | Error _ -> failwith "Failed to create storage"
  | Ok storage ->
    match Manipulation.Memory.create_database storage ~name:"sakura" with
    | Error _ -> failwith "Failed to create initial database"
    | Ok db ->
      let db = register_prelude_relations storage db in
      let db_ref = ref db in
      Printf.printf "Database hash: %s\n%!" (short_hash db.Management.Database.hash);
      List.iter (fun cmd -> print_endline @@ execute_command storage db_ref cmd)
        n_way;
      ()

let () =
  let port = default_port in
  match Management.Physical.Memory.create () with
  | Error _ -> failwith "Failed to create storage"
  | Ok storage ->
    match Manipulation.Memory.create_database storage ~name:"sakura" with
    | Error _ -> failwith "Failed to create initial database"
    | Ok db ->
      let db = register_prelude_relations storage db in
      let db_ref = ref db in
      let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
      Unix.setsockopt sock Unix.SO_REUSEADDR true;
      Unix.setsockopt sock Unix.SO_REUSEPORT true;
      Unix.bind sock (Unix.ADDR_INET (Unix.inet_addr_loopback, port));
      Unix.listen sock 5;
      Printf.printf "Sakura server listening on 127.0.0.1:%d\n" port;
      Printf.printf "Database hash: %s\n%!" (short_hash db.Management.Database.hash);
      while true do
        let (client_fd, _addr) = Unix.accept sock in
        handle_client storage db_ref client_fd;
        (try Unix.close client_fd with _ -> ())
      done