aboutsummaryrefslogblamecommitdiffstats
path: root/bin/server.ml
blob: a7b5fa3b1880a9f928d6c67f6dfcc65ca895a0ce (plain) (tree)





























































































































































































































































                                                                                       
open Relational_engine

let default_port = 7777
let default_limit = 50

(* ============================================================================
   Minimal JSON serialisation — no external deps
   ============================================================================ *)

let js_str s =
  let buf = Buffer.create (String.length s + 2) in
  Buffer.add_char buf '"';
  String.iter (function
    | '"'  -> Buffer.add_string buf {|\"|}
    | '\\' -> Buffer.add_string buf {|\\|}
    | '\n' -> Buffer.add_string buf {|\n|}
    | '\r' -> Buffer.add_string buf {|\r|}
    | '\t' -> Buffer.add_string buf {|\t|}
    | c    -> Buffer.add_char buf c) s;
  Buffer.add_char buf '"';
  Buffer.contents buf

let js_obj pairs =
  "{" ^
  String.concat ","
    (List.map (fun (k, v) -> js_str k ^ ":" ^ v) pairs) ^
  "}"

let js_arr elems = "[" ^ String.concat "," elems ^ "]"
let js_bool b    = if b then "true" else "false"
let js_int  n    = string_of_int n

(* ============================================================================
   Value serialisation
   ============================================================================ *)

(** Convert an AbstractValue (Obj.t) to a JSON fragment.
    Uses Obj tag inspection to recover Int / String / Float. *)
let abstract_to_json (v : Conventions.AbstractValue.t) =
  if Obj.is_int v then js_int (Obj.obj v : int)
  else
    let tag = Obj.tag v in
    if tag = Obj.string_tag then js_str (Obj.obj v : string)
    else if tag = Obj.double_tag then
      let f = (Obj.obj v : float) in
      if Float.is_nan f || Float.is_infinite f then "null"
      else string_of_float f
    else js_str "<opaque>"

let tuple_to_json (t : Tuple.materialized) =
  let pairs = Tuple.AttributeMap.bindings t.Tuple.attributes in
  js_obj (List.map (fun (k, attr) -> (k, abstract_to_json attr.Attribute.value)) pairs)

(* ============================================================================
   Relation materialisation
   ============================================================================ *)

(** Iterate a generator up to [limit] tuples; returns (rows, truncated). *)
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

(** Materialise either a stored or generator-backed relation. *)
let materialize_relation storage (rel : Relation.t) limit =
  let gen = Algebra.Memory.to_generator storage rel in
  materialize_generator gen limit

(* ============================================================================
   JSON response builders
   ============================================================================ *)

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

let relation_to_json storage (db_hash : string) (rel : Relation.t) limit =
  let schema_json =
    js_arr (List.map (fun (a, d) ->
      js_obj [("attr", js_str a); ("domain", js_str d)]
    ) rel.Relation.schema)
  in
  let (tuples, truncated) = materialize_relation storage rel limit in
  let rows_json = js_arr (List.map tuple_to_json tuples) in
  js_obj [
    ("type",      js_str "relation");
    ("name",      js_str rel.Relation.name);
    ("schema",    schema_json);
    ("rows",      rows_json);
    ("row_count", js_int (List.length tuples));
    ("truncated", js_bool truncated);
    ("db_hash",   js_str (short_hash db_hash));
  ]

let ok_response db_hash msg =
  js_obj [
    ("type",    js_str "ok");
    ("message", js_str msg);
    ("db_hash", js_str (short_hash db_hash));
  ]

let error_response db_hash msg =
  js_obj [
    ("type",    js_str "error");
    ("message", js_str msg);
    ("db_hash", js_str (short_hash db_hash));
  ]

(* ============================================================================
   Error formatting
   ============================================================================ *)

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

(* ============================================================================
   Command execution
   ============================================================================ *)

let execute_command storage db_ref cmd =
  let db = !db_ref in
  let h  = db.Management.Database.hash in
  (* 0. Special commands *)
  let cmd = match String.trim cmd with
    | "(schema)" -> {|(Base sakura:attribute)|}
    | s -> s
  in
  (* 1. Try DRL *)
  match Drl.Parser.of_string cmd with
  | Ok query ->
    (match Drl.Executor.Memory.execute storage db query with
     | Ok rel -> relation_to_json 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 _ ->
    (* 2. Try DML *)
    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 _ ->
      (* 3. Try DCL *)
      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"

(* ============================================================================
   Prelude registration
   ============================================================================ *)

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
    ]

(* ============================================================================
   TCP server
   ============================================================================ *)

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 = execute_command storage db_ref line in
         output_string oc (response ^ "\n");
         flush oc
       end
     done
   with End_of_file | Unix.Unix_error _ -> ())

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