diff --git a/lib/stdlib/apis/postgres/postgres.opa b/lib/stdlib/apis/postgres/postgres.opa index dd043893..9f1065ee 100644 --- a/lib/stdlib/apis/postgres/postgres.opa +++ b/lib/stdlib/apis/postgres/postgres.opa @@ -199,7 +199,8 @@ type Postgres.connection = { rows : int /** The number of rows received during a query */ rowdescs : Postgres.rowdescs /** Stored value of the last row description received */ paramdescs : list(int) /** List of the last-received parameter descriptions */ - handlers : intmap((OpaType.ty,Postgres.abstract_handler)) /** Handlers for unknown data types */ + handlers : intmap((string,OpaType.ty,Postgres.abstract_handler)) /** Handlers for unknown data types */ + backhandlers : stringmap(int) /** Reverse map for outgoing data */ } /** Defines whether an operation is for a prepared statement or a portal */ @@ -264,7 +265,7 @@ Postgres = {{ params=StringMap.empty processid=-1 secret_key=-1 query="" status="" suspended=false error=none empty=true completed=[] paramdescs=[] rows=0 rowdescs=[] - handlers=IntMap.empty }} + handlers=IntMap.empty backhandlers=StringMap.empty }} | {~failure} -> {~failure} /** Close a connection object. @@ -343,11 +344,17 @@ Postgres = {{ * * @param conn The connection object. * @param type_id The PostgreSQL type id number, can be read from the server. + * @param name The type name. * @param handler A handler function, should return an option of an opa value corresponding to the type. * @returns An updated connection with the handler installed. */ - install_handler(conn:Postgres.connection, type_id:Postgres.type_id, handler:Postgres.handler('a)) : Postgres.connection = - {conn with handlers=IntMap.add(type_id,(PostgresTypes.name_type(@typeval('a)),@unsafe_cast(handler)),conn.handlers)} + install_handler(conn:Postgres.connection, type_id:Postgres.type_id, name:string, handler:Postgres.handler('a)) + : Postgres.connection = + nty = PostgresTypes.name_type(@typeval('a)) + {conn with + handlers=IntMap.add(type_id,(name,nty,@unsafe_cast(handler)),conn.handlers) + backhandlers=StringMap.add("{nty}",type_id,conn.backhandlers) + } /** Set the protocol minor version number. * @@ -736,7 +743,7 @@ Postgres = {{ * * @param conn The connection object. * @param name The name of the type. - * @returns A tuple of the updated connection (maybe with and [error] value) and the type_id (-1=not found). + * @returns A tuple of the updated connection (maybe with an [error] value) and the type_id ([-1] means "not found"). */ get_type_id(conn:Postgres.connection, name:string) : (Postgres.connection,int) = type_id = ServerReference.create(-1) @@ -758,61 +765,74 @@ Postgres = {{ * @param dummy A dummy optional value of the Opa type (used to analyse the field names). * @returns A Postgres result type with updated connection plus possible failure code. */ - create_enum(conn:Postgres.connection, _:option('a)) : Postgres.result = - raw_ty = @typeval('a) + create_enum(conn:Postgres.connection, _:option('a)) : Postgres.result = create_enum_ty(conn, @typeval('a)) + create_enum_ty(conn:Postgres.connection, raw_ty:OpaType.ty) : Postgres.result = match raw_ty with | {TyName_args=[]; TyName_ident=name} -> ty = PostgresTypes.name_type(raw_ty) - match ty with - | {TySum_col=col ...} -> - (names,err) = - List.fold((row, (names,err) -> - match err with - | {some=_} -> (names,err) - | {none} -> - match row with - | [~{label; ty}] -> - if OpaType.is_void(ty) - then ([label|names],err) - else (names,{some="create_enum: Label {label} is not void ({ty})"}) - | _ -> (names,{some="create_enum: Bad type for enum {row}"}) - end),col,([],none)) - match err with - | {none} -> - match - match get_type_id(conn, name) with - | (conn,-1) -> - qnames = String.concat(", ",List.map((name -> "'{name}'"),names)) - conn = query(conn,"CREATE TYPE {name} AS ENUM ({qnames})",default_listener) - if Option.is_none(conn.error) - then get_type_id(conn, name) - else (conn,-1) - | (conn,type_id) -> (conn,type_id) - end - with - | (conn,-1) -> {failure=(conn,{bad_type="create_enum: can't create enum for type {ty}"})} - | (conn,type_id) -> - generic_handler(h_type_id:Postgres.type_id, h_ty:OpaType.ty, val:Postgres.data) : option(void) = - if type_id == h_type_id && ty == h_ty + match PostgresTypes.enum_field_names(ty) with + | {success=names} -> + match + match get_type_id(conn, name) with + | (conn,-1) -> + qnames = String.concat(", ",List.map((name -> "'{name}'"),names)) + conn = query(conn,"CREATE TYPE {name} AS ENUM ({qnames})",default_listener) + if Option.is_none(conn.error) + then get_type_id(conn, name) + else (conn,-1) + | (conn,type_id) -> (conn,type_id) + end + with + | (conn,-1) -> {failure=(conn,{bad_type="create_enum: can't create enum for type {ty}"})} + | (conn,type_id) -> + generic_handler(h_type_id:Postgres.type_id, h_ty:OpaType.ty, val:Postgres.data) : option(void) = + if type_id == h_type_id && ty == h_ty + then + val = match val with ~{text} -> text | ~{binary} -> string_of_binary(binary) + if List.mem(val,names) then - val = match val with ~{text} -> text | ~{binary} -> string_of_binary(binary) - if List.mem(val,names) - then - match OpaValue.Record.field_of_name(val) with - | {none} -> none - | {some=field} -> {some=@unsafe_cast(OpaValue.Record.make_simple_record(field))} - end - else none + match OpaValue.Record.field_of_name(val) with + | {none} -> none + | {some=field} -> {some=@unsafe_cast(OpaValue.Record.make_simple_record(field))} + end else none - do Log.notice("Postgres.create_enum","installing handler for type_id {type_id} type {OpaType.to_pretty(raw_ty)}") - {success={conn with handlers=IntMap.add(type_id,(ty,@unsafe_cast(generic_handler)),conn.handlers)}} - end - | {some=err} -> {failure=(conn,{bad_type=err})} - end - | _ -> {failure=(conn,{bad_type="create_enum: bad type {ty}"})} + else none + do Log.notice("Postgres.create_enum","installing handler for type_id {type_id} type {OpaType.to_pretty(raw_ty)}") + {success={conn with + handlers=IntMap.add(type_id,(name,ty,@unsafe_cast(generic_handler)),conn.handlers) + backhandlers=StringMap.add("{ty}",type_id,conn.backhandlers) + }} + end + | {~failure} -> {failure=(conn,{bad_type=failure})} end | _ -> {failure=(conn,{bad_type="create_enum: bad type {raw_ty}"})} + fold(f:Postgres.connection, 'a -> Postgres.result, conn:Postgres.connection, l:list('a)) : Postgres.result = + rec aux(conn, l) = + match l with + | [v|l] -> + match f(conn, v) with + | {success=conn} -> aux(conn, l) + | {~failure} -> {~failure} + end + | [] -> {success=conn} + end + aux(conn, l) + + create_table(conn:Postgres.connection, dbase:string, temp:bool, value:'a, k:Postgres.listener) : Postgres.connection = + enums = PostgresTypes.record_enum_types(@typeval('a)) + do jlog("enums:{enums}") + known_enums = IntMap.fold((_, (name,_,_), names -> [name|names]),conn.handlers,[]) + new_enums = List.filter(((name, _) -> not(List.mem(name,known_enums))),enums) + do jlog("new_enums:{new_enums}") + match fold(create_enum_ty, conn, List.map((x -> x.f2),new_enums)) with + | {success=conn} -> query(conn,PostgresTypes.create(conn, dbase, temp, value),k) + | {failure=(conn,failure)} -> error(conn,failure,k) + end + + insert(conn:Postgres.connection, dbase:string, value:'a, k:Postgres.listener) : Postgres.connection = + query(conn,PostgresTypes.insert(conn, dbase, value),k) + }} // End of file postgres.opa diff --git a/lib/stdlib/apis/postgres/types.opa b/lib/stdlib/apis/postgres/types.opa index 063cb376..93e4a623 100644 --- a/lib/stdlib/apis/postgres/types.opa +++ b/lib/stdlib/apis/postgres/types.opa @@ -22,24 +22,49 @@ type Postgres.type_id = int // for clarity +type Postgres.smallint = int +//type Postgres.bigint = int64 // ??? not really necessary +type Postgres.real = float + +type Postgres.bytea = binary + type Postgres.money = {currency:option(string); amount:float} type Postgres.numeric = {pre:string; post:string} // TODO: arbitrary precision numbers in Opa +type Postgres.date = Date.date +type Postgres.time = Date.date +type Postgres.timestamp = Date.date +type Postgres.timestamptz = Date.date + type Postgres.opatype = {Null} / {Int:int} + / {Int16:int} / {Int64:int64} / {Bool:bool} / {String:string} + / {Real:float} / {Float:float} / {Money:Postgres.money} / {Numeric:Postgres.numeric} / {Date:Date.date} - / {Binary:binary} + / {Time:Postgres.time} + / {Timestamp:Postgres.timestamp} + / {Timestamptz:Postgres.timestamptz} + / {Bytea:binary} / {IntArray1:list(int)} / {IntArray2:list(list(int))} / {IntArray3:list(list(list(int)))} + / {Int16Array1:list(int)} + / {Int16Array2:list(list(int))} + / {Int16Array3:list(list(list(int)))} + / {Int64Array1:list(int64)} + / {Int64Array2:list(list(int64))} + / {Int64Array3:list(list(list(int64)))} + / {RealArray1:list(float)} + / {RealArray2:list(list(float))} + / {RealArray3:list(list(list(float)))} / {FloatArray1:list(float)} / {FloatArray2:list(list(float))} / {FloatArray3:list(list(list(float)))} @@ -52,9 +77,12 @@ type Postgres.opatype = / {BadText:string} / {BadCode:(int,binary)} / {BadDate:string} + / {BadEnum:list(string)} type Postgres.oparow = stringmap(Postgres.opatype) +type Postgres.oparowl = list((string,Postgres.opatype)) + type Postgres.handler('a) = Postgres.type_id, OpaType.ty, Postgres.data -> option('a) type Postgres.abstract_handler = Postgres.handler(void) @@ -101,14 +129,17 @@ PostgresTypes = {{ ms = Option.default(0,ms) do_tz(Date.build(~{year=1970;month={january};day=1;h;min;s;ms}),tz) - getTextDate(s:string) : Postgres.opatype = - match Parser.try_parse(date1p, s) with - | {some=date} -> {Date=date} - | {none} -> {BadDate=s} + getTextDate(type_id:int, s:string) : Postgres.opatype = + match (type_id,Parser.try_parse(date1p, s)) with + | (1082,{some=date}) -> {Date=date} + | (1083,{some=time}) -> {Time=time} + | (1114,{some=timestamp}) -> {Timestamp=timestamp} + | (1184,{some=timestamptz}) -> {Timestamptz=timestamptz} + | _ -> {BadDate=s} getTextByteA(s:string) : Postgres.opatype = if String.has_prefix("\\x",s) - then {Binary=Binary.of_hex(String.sub(2,String.length(s)-2,s))} + then {Bytea=Binary.of_hex(String.sub(2,String.length(s)-2,s))} else @fail("TODO: bytea escape format") @private comma = parser "," -> void @@ -153,6 +184,30 @@ PostgresTypes = {{ getTextIntArray = getTextArray(_, Rule.integer, mkintarray) + @private mkint16array(a) : Postgres.opatype = + match a with + | {~Array1} -> {Int16Array1=Array1} + | {~Array2} -> {Int16Array2=Array2} + | {~Array3} -> {Int16Array3=Array3} + + getTextInt16Array = getTextArray(_, Rule.integer, mkint16array) // TODO: check range + + @private mkint64array(a) : Postgres.opatype = + match a with + | {~Array1} -> {Int64Array1=Array1} + | {~Array2} -> {Int64Array2=Array2} + | {~Array3} -> {Int64Array3=Array3} + + getTextInt64Array = getTextArray(_, Rule.int64, mkint64array) + + @private mkrealarray(a) : Postgres.opatype = + match a with + | {~Array1} -> {RealArray1=Array1} + | {~Array2} -> {RealArray2=Array2} + | {~Array3} -> {RealArray3=Array3} + + getTextRealArray = getTextArray(_, Rule.float, mkrealarray) + @private mkfloatarray(a) : Postgres.opatype = match a with | {~Array1} -> {FloatArray1=Array1} @@ -170,7 +225,7 @@ PostgresTypes = {{ @private stringchp = parser "\\\"" -> Text.cons("\\\"") | ch=(!"\"" .) -> ch @private stringp = parser "\"" str=(stringchp)* "\"" -> Text.to_string(Text.ltconcat(str)) - | str=(!"," .)* -> Text.to_string(Text.ltconcat(str)) + | str=(!"," !"}" .)* -> Text.to_string(Text.ltconcat(str)) getTextStringArray = getTextArray(_, stringp, mkstringarray) @@ -231,14 +286,20 @@ PostgresTypes = {{ | 16 -> add({Bool=(text=="t")}) | 17 -> add(getTextByteA(text)) | 20 -> add({Int64=Int64.of_string(text)}) - | 21 | 23 | 26 -> add({Int=Int.of_string(text)}) + | 21 -> add({Int16=Int.of_string(text)}) + | 23 | 26 -> add({Int=Int.of_string(text)}) + | 25 -> add({String=text}) | 790 -> add(getTextMoney(text)) - | 700 | 701 -> add({Float=Float.of_string(text)}) + | 700 -> add({Real=Float.of_string(text)}) + | 701 -> add({Float=Float.of_string(text)}) | 1042 | 1043 -> add({String=text}) - | 1082 | 1083 | 1114 | 1184 -> add(getTextDate(text)) - | 1005 | 1007 | 1016 | 1231 -> add(getTextIntArray(text)) - | 1015 -> add(getTextStringArray(text)) - | 1021 | 1022 -> add(getTextFloatArray(text)) + | 1082 | 1083 | 1114 | 1184 -> add(getTextDate(rowdesc.type_id,text)) + | 1005 -> add(getTextInt16Array(text)) + | 1007 | 1231 -> add(getTextIntArray(text)) + | 1016 -> add(getTextInt64Array(text)) + | 1009 | 1015 -> add(getTextStringArray(text)) + | 1021 -> add(getTextRealArray(text)) + | 1022 -> add(getTextFloatArray(text)) | 1186 -> add(getTextInterval(text)) | 1700 -> add(getTextNumeric(text)) | id -> add({TypeId=(id,~{text})}) @@ -257,6 +318,287 @@ PostgresTypes = {{ | {some={TypeId=(type_id,data)}} -> handler(type_id,name_type(@typeval('a)),data) | _ -> {none} + rec_to_postgres(conn:Postgres.connection, v:'a, fields:OpaType.fields): Postgres.oparowl = + List.flatten(OpaValue.Record.fold_with_fields((field, tyfield, value, oparowl -> + name = OpaValue.Record.name_of_field_unsafe(field) + res = opa_to_pg(conn, name, value, tyfield) + [res | oparowl]), v, fields, [])) + + error(msg) = do Log.error("Postgres.opa_to_postgres", msg) @fail(msg) + + enum_field_names(ty:OpaType.ty) : outcome(list(string),string) = + match ty with + | {TySum_col=col ...} -> + List.fold((row, result -> + match result with + | {~failure} -> {~failure} + | {success=names} -> + match row with + | [~{label; ty}] -> + if OpaType.is_void(ty) + then {success=[label|names]} + else {failure="enum_field_names: Label {label} is not void ({ty})"} + | _ -> {failure="enum_field_names: Bad type {ty}"} + end),col,{success=[]}) + | _ -> {failure="enum_field_names: Bad type {ty}"} + end + + record_enum_types(ty:OpaType.ty) : list((string,OpaType.ty)) = + match name_type(ty) with + | {TyRecord_row=row ...} -> + List.fold(({label=_; ty=rty}, enums -> + match rty with + | {TyName_args=[]; TyName_ident=name} -> + nrty = name_type(rty) + match enum_field_names(nrty) with + | {success=_} -> [(name,rty)|enums] + | {failure=_} -> enums + end + | _ -> enums + end),row,[]) + | _ -> [] + + enum_to_pg(type_id:int, key:string, v:'a, ty:OpaType.ty) : Postgres.oparowl = + //do jlog("enum_to_pg: type_id={type_id} key={key} v={v} ty={ty}") + field_names = %%BslNativeLib.record_fields%%(v) + //do jlog("field_names:{field_names}") + match field_names with + | [] -> [(key,{Null})] + | [name] -> + match enum_field_names(ty) with + | {success=enames} -> + //do jlog("enames:{enames}") + if List.mem(name,enames) + then [(key,{TypeId=(type_id,{text=name})})] + else [(key,{BadEnum=[name]})] + | {failure=_} -> [(key,{BadEnum=[name]})] + end + | _ -> [(key,{BadEnum=field_names})] + + list_to_pg(key:string, v:'a, ty:OpaType.ty, level:int) : Postgres.oparowl = + match (level,ty) with + | (_,{TyName_args=[ty]; TyName_ident="list"}) -> list_to_pg(key, v, ty, level+1) + | (1,{TyConst={TyInt={}}}) -> [(key,{IntArray1=@unsafe_cast(v)})] + | (2,{TyConst={TyInt={}}}) -> [(key,{IntArray2=@unsafe_cast(v)})] + | (3,{TyConst={TyInt={}}}) -> [(key,{IntArray3=@unsafe_cast(v)})] + | (1,{TyName_args=[]; TyName_ident="Postgres.smallint"}) -> [(key,{Int16Array1=@unsafe_cast(v)})] + | (2,{TyName_args=[]; TyName_ident="Postgres.smallint"}) -> [(key,{Int16Array2=@unsafe_cast(v)})] + | (3,{TyName_args=[]; TyName_ident="Postgres.smallint"}) -> [(key,{Int16Array3=@unsafe_cast(v)})] + | (1,{TyName_args=[]; TyName_ident="int64"}) -> [(key,{Int64Array1=@unsafe_cast(v)})] + | (2,{TyName_args=[]; TyName_ident="int64"}) -> [(key,{Int64Array2=@unsafe_cast(v)})] + | (3,{TyName_args=[]; TyName_ident="int64"}) -> [(key,{Int64Array3=@unsafe_cast(v)})] + | (1,{TyConst={TyString={}}}) -> [(key,{StringArray1=@unsafe_cast(v)})] + | (2,{TyConst={TyString={}}}) -> [(key,{StringArray2=@unsafe_cast(v)})] + | (3,{TyConst={TyString={}}}) -> [(key,{StringArray3=@unsafe_cast(v)})] + | (1,{TyName_args=[]; TyName_ident="Postgres.real"}) -> [(key,{RealArray1=@unsafe_cast(v)})] + | (2,{TyName_args=[]; TyName_ident="Postgres.real"}) -> [(key,{RealArray2=@unsafe_cast(v)})] + | (3,{TyName_args=[]; TyName_ident="Postgres.real"}) -> [(key,{RealArray3=@unsafe_cast(v)})] + | (1,{TyConst={TyFloat={}}}) -> [(key,{FloatArray1=@unsafe_cast(v)})] + | (2,{TyConst={TyFloat={}}}) -> [(key,{FloatArray2=@unsafe_cast(v)})] + | (3,{TyConst={TyFloat={}}}) -> [(key,{FloatArray3=@unsafe_cast(v)})] + | _ -> error("unknown value {Debug.dump(v)} of type {OpaType.to_pretty(ty)}") + + opa_to_pg(conn:Postgres.connection, key:string, v:'a, ty:OpaType.ty) : Postgres.oparowl = + v = Magic.id(v) + match ty with + | {TyName_args=[]; TyName_ident="void"} -> [(key,{Null})] + | {TyConst={TyInt={}}} -> [(key,{Int=(@unsafe_cast(v):int)})] + | {TyConst={TyString={}}} -> [(key,{String=(@unsafe_cast(v):string)})] + | {TyConst={TyFloat={}}} -> [(key,{Float=(@unsafe_cast(v):float)})] + | {TyName_args=[]; TyName_ident="Postgres.real"} -> [(key,{Real=(@unsafe_cast(v):float)})] + | {TyName_args=[]; TyName_ident="Postgres.smallint"} -> [(key,{Int16=(@unsafe_cast(v):int)})] + | {TyName_args=[]; TyName_ident="int64"} -> [(key,{Int64=(@unsafe_cast(v):int64)})] + | {TyName_args=[]; TyName_ident="bool"} -> [(key,{Bool=(@unsafe_cast(v):bool)})] + | {TyName_args=[]; TyName_ident="Postgres.date"} + | {TyName_args=[]; TyName_ident="Date.date"} -> [(key,{Date=(@unsafe_cast(v):Date.date)})] + | {TyName_args=[]; TyName_ident="Postgres.time"} -> [(key,{Time=(@unsafe_cast(v):Date.date)})] + | {TyName_args=[]; TyName_ident="Postgres.timestamp"} -> [(key,{Timestamp=(@unsafe_cast(v):Date.date)})] + | {TyName_args=[]; TyName_ident="Postgres.timestamptz"} -> [(key,{Timestamptz=(@unsafe_cast(v):Date.date)})] + | {TyName_args=[]; TyName_ident="Postgres.bytea"} + | {TyName_args=[]; TyName_ident="binary"} -> [(key,{Bytea=(@unsafe_cast(v):binary)})] //TODO + | {TyName_args=[]; TyName_ident="Postgres.money"} -> [(key,{Money=(@unsafe_cast(v):Postgres.money)})] + | {TyName_args=[]; TyName_ident="Postgres.numeric"} -> [(key,{Numeric=(@unsafe_cast(v):Postgres.numeric)})] + | {TyName_args=[]; TyName_ident="Duration.duration"} -> [(key,{Duration=(@unsafe_cast(v):Duration.duration)})] + | {TyName_args=_; TyName_ident="Postgres.oparow"} -> + @unsafe_cast(StringMap.fold((k, v, l -> [(k,v)|l]),@unsafe_cast(v),[])):Postgres.oparowl + | {TyName_args=_; TyName_ident="Postgres.oparowl"} -> @unsafe_cast(v):Postgres.oparowl + | {TyName_args=[ty]; TyName_ident="list"} -> list_to_pg(key, v, ty, 1) + | {TyName_args=[ty]; TyName_ident="option"} -> + match (@unsafe_cast(v):option('a)) with + | {some=v} -> opa_to_pg(conn, key, v, ty) + | {none} -> [(key,{Null})] + end + | {TyName_args=tys; TyName_ident=tyid} -> opa_to_pg(conn, key, v, OpaType.type_of_name(tyid, tys)) + | _ -> + match StringMap.get("{ty}",conn.backhandlers) with + | {some=type_id} -> enum_to_pg(type_id, key, v, ty) + | {none} -> error("unknown value {Debug.dump(v)} of type {OpaType.to_pretty(ty)}") + end + + opa_to_postgres(conn:Postgres.connection, v:'a, ty_opt:option(OpaType.ty)) : Postgres.oparowl = + ty = match ty_opt with {some=ty} -> ty | {none} -> @typeof(v) + match name_type(ty) with + | {TyRecord_row=row ...} -> + (match row with + | [] -> [("value",{Null})] + | [{label=name; ty=ty}] -> + if OpaType.is_void(ty) + then [(name,{Null})] + else rec_to_postgres(conn, v, row) + | _ -> rec_to_postgres(conn, v, row)) + | {TySum_col=col ...} -> + if List.mem([{label="false"; ty={TyRecord_row=[]}}],col) + then [("value",{Bool=@unsafe_cast(v)})] + else rec_to_postgres(conn, v, OpaType.fields_of_fields_list(v, col).f1) + | ty -> + opa_to_pg(conn, "value", v, ty) + + of_opa(conn:Postgres.connection, v:'a) : Postgres.oparowl = opa_to_postgres(conn,v,{some=@typeval('a)}) + + ls(l,tos) = List.to_string_using("\{","}",",",List.map(tos,l)) + lls(l,tos) = List.to_string_using("\{","}",",",List.map(ls(_,tos),l)) + llls(l,tos) = List.to_string_using("\{","}",",",List.map(lls(_,tos),l)) + encode_string(s) = //TODO!!! + if String.contains(s,",") + then "\"{s}\"" + else s + + @private tzfmt = Date.generate_printer("%z") + @private timestamp_fmt = Date.generate_printer("%F %T.%x") + + timestamp_to_string(d:Date.date) : string = Date.to_formatted_string(timestamp_fmt, d) + timestamptz_to_string(d:Date.date) : string = + tz = Date.to_formatted_string(tzfmt, d) + tz = // timezone with colons is missing from formatted dates + match String.length(tz) with + | 4 -> String.sub(0,2,tz)^":"^String.sub(2,2,tz) + | 5 -> String.sub(0,3,tz)^":"^String.sub(3,2,tz) + | _ -> tz + end + "{Date.to_formatted_string(timestamp_fmt, d)}{tz}" + + duration_to_iso8601(d:Duration.duration) : string = + hr = Duration.to_human_readable(d) + sign = if hr.forward then 1 else -1 + get(code,amnt) = if amnt == 0 then "" else "{sign*amnt}{code}" + t = if hr.h != 0 || hr.min != 0 || hr.s != 0 then "T" else "" + "P{get("Y",hr.year)}{get("M",hr.month)}{get("D",hr.day)}{t}{get("H",hr.h)}{get("M",hr.min)}{get("S",hr.s)}" + + string_of_field_value(opatype:Postgres.opatype) : string = + match opatype with + | {Null} -> "null" + | {Int=int} -> Int.to_string(int) + | {Int16=int} -> Int.to_string(int) + | {Int64=int64} -> Int64.to_string(int64) + | {Bool=bool} -> Bool.to_string(bool) + | {String=string} -> "'{encode_string(string)}'" + | {Real=float} -> Float.to_string(float) + | {Float=float} -> Float.to_string(float) + | {Money=money} -> "'{money.currency?""}{money.amount}'" + | {Numeric=numeric} -> "{numeric.pre}.{numeric.post}" + | {Date=date} -> "'{Date.to_formatted_string(Date.date_only_printer,date)}'" + | {Time=time} -> "'{Date.to_formatted_string(Date.time_only_printer,time)}'" + | {Timestamp=timestamp} -> "'{timestamp_to_string(timestamp)}'" + | {Timestamptz=timestamptz} -> "'{timestamptz_to_string(timestamptz)}'" + | {Bytea=binary} -> "'{string_of_binary(binary)}'" //TODO!!! + | {IntArray1=l} -> "'{ls(l,Int.to_string)}'" + | {IntArray2=ll} -> "'{lls(ll,Int.to_string)}'" + | {IntArray3=lll} -> "'{llls(lll,Int.to_string)}'" + | {Int16Array1=l} -> "'{ls(l,Int.to_string)}'" + | {Int16Array2=ll} -> "'{lls(ll,Int.to_string)}'" + | {Int16Array3=lll} -> "'{llls(lll,Int.to_string)}'" + | {Int64Array1=l} -> "'{ls(l,Int64.to_string)}'" + | {Int64Array2=ll} -> "'{lls(ll,Int64.to_string)}'" + | {Int64Array3=lll} -> "'{llls(lll,Int64.to_string)}'" + | {RealArray1=l} -> "'{ls(l,Float.to_string)}'" + | {RealArray2=ll} -> "'{lls(ll,Float.to_string)}'" + | {RealArray3=lll} -> "'{llls(lll,Float.to_string)}'" + | {FloatArray1=l} -> "'{ls(l,Float.to_string)}'" + | {FloatArray2=ll} -> "'{lls(ll,Float.to_string)}'" + | {FloatArray3=lll} -> "'{llls(lll,Float.to_string)}'" + | {StringArray1=l} -> "'{ls(l,encode_string)}'" + | {StringArray2=ll} -> "'{lls(ll,encode_string)}'" + | {StringArray3=lll} -> "'{llls(lll,encode_string)}'" + | {Duration=duration} -> "'{duration_to_iso8601(duration)}'" + | {TypeId=(_,data)} -> + match data with + | {~text} -> "'{text}'" + | {~binary} -> "'{string_of_binary(binary)}'" + end + | {BadData=_} -> "null" + | {BadText=_} -> "null" + | {BadCode=_} -> "null" + | {BadDate=_} -> "null" + | {BadEnum=_} -> "null" + + postgres_type(conn:Postgres.connection, opatype:Postgres.opatype) : string = + match opatype with + | {Null} -> "int" // arbitrary, this can actually be anything + | {Int=_} -> "int" + | {Int16=_} -> "smallint" + | {Int64=_} -> "bigint" + | {Bool=_} -> "bool" + | {String=_} -> "text" + | {Real=_} -> "float4" + | {Float=_} -> "float8" + | {Money=_} -> "money" + | {Numeric=_} -> "numeric" + | {Date=_} -> "date" + | {Time=_} -> "time" + | {Timestamp=_} -> "timestamp" + | {Timestamptz=_} -> "timestamptz" + | {Bytea=_} -> "bytea" + | {IntArray1=_} -> "int[]" + | {IntArray2=_} -> "int[][]" + | {IntArray3=_} -> "int[][][]" + | {Int16Array1=_} -> "int2[]" + | {Int16Array2=_} -> "int2[][]" + | {Int16Array3=_} -> "int2[][][]" + | {Int64Array1=_} -> "int8[]" + | {Int64Array2=_} -> "int8[][]" + | {Int64Array3=_} -> "int8[][][]" + | {RealArray1=_} -> "float4[]" + | {RealArray2=_} -> "float4[][]" + | {RealArray3=_} -> "float4[][][]" + | {FloatArray1=_} -> "float8[]" + | {FloatArray2=_} -> "float8[][]" + | {FloatArray3=_} -> "float8[][][]" + | {StringArray1=_} -> "text[]" + | {StringArray2=_} -> "text[][]" + | {StringArray3=_} -> "text[][][]" + | {Duration=_} -> "interval" + | {TypeId=(type_id,_)} -> + match IntMap.get(type_id,conn.handlers) with + | {some=(name,_,_)} -> name + | {none} -> "int" + end + | {BadData=_} -> "int" + | {BadText=_} -> "int" + | {BadCode=_} -> "int" + | {BadDate=_} -> "int" + | {BadEnum=_} -> "int" + + field_names(oparowl:Postgres.oparowl) : list(string) = + List.map(((key,_) -> key),oparowl) + + field_values(oparowl:Postgres.oparowl) : list(string) = + List.map(((_,val) -> string_of_field_value(val)),oparowl) + + field_types(conn:Postgres.connection, oparowl:Postgres.oparowl) : list(string) = + List.map(((_,val) -> postgres_type(conn,val)),oparowl) + + @private csl(l) = String.concat(",",l) + + create(conn:Postgres.connection, dbase:string, temp:bool, v:'a) : string = + row = of_opa(conn, v) + flds = List.map2((n, t -> "{n} {t}"),field_names(row),field_types(conn,row)) + temp = if temp then "TEMP " else "" + "CREATE {temp}TABLE {dbase}({csl(flds)})" + + insert(conn:Postgres.connection, dbase:string, v:'a) : string = + row = of_opa(conn, v) + "INSERT INTO {dbase}({csl(field_names(row))}) VALUES({csl(field_values(row))})" + @private StringMap_keys(sm) : list(string) = StringMap.fold((k, _, keys -> [k|keys]),sm,[]) @private intersect(l1, l2) = @@ -281,6 +623,11 @@ PostgresTypes = {{ | {TyName_args=[]; TyName_ident="Postgres.money"} | {TyName_args=[]; TyName_ident="Postgres.numeric"} | {TyName_args=[]; TyName_ident="Date.date"} + | {TyName_args=[]; TyName_ident="Postgres.date"} + | {TyName_args=[]; TyName_ident="Postgres.time"} + | {TyName_args=[]; TyName_ident="Postgres.timestamp"} + | {TyName_args=[]; TyName_ident="Postgres.timestamptz"} + | {TyName_args=[]; TyName_ident="Postgres.bytea"} | {TyName_args=[]; TyName_ident="binary"} | {TyName_args=[_]; TyName_ident="option"} | {TyName_args=[]; TyName_ident="bool"} @@ -397,12 +744,80 @@ PostgresTypes = {{ | {some=opatype} -> opatype_to_opa(opatype, ty, dflt) | {none} -> error("Missing name {name}") + and opatype_to_list(opatype:Postgres.opatype, ty:OpaType.ty, dflt:option('a), level:int): option('a) = + err(name) = + pre = String.repeat(level,"list(") + post = String.repeat(level,")") + error("expected {pre}{name}{post}, got {opatype}") + list(get,name) = + match opatype with + | {Null} -> dflt + | opatype -> + match get(opatype) with + | {some=a} -> {some=@unsafe_cast(a)} + | {none} -> err(name) + end + end + match (level,ty) with + | (_,{TyName_args=[ty]; TyName_ident="list"}) -> + opatype_to_list(opatype, ty, dflt, level+1) + | (1,{TyConst={TyInt={}}}) -> + list((ot -> match ot with {IntArray1=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"int") + | (2,{TyConst={TyInt={}}}) -> + list((ot -> match ot with {IntArray2=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"int") + | (3,{TyConst={TyInt={}}}) -> + list((ot -> match ot with {IntArray3=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"int") + | (1,{TyName_args=[]; TyName_ident="Postgres.smallint"}) -> + list((ot -> match ot with {Int16Array1=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"smallint") + | (2,{TyName_args=[]; TyName_ident="Postgres.smallint"}) -> + list((ot -> match ot with {Int16Array2=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"smallint") + | (3,{TyName_args=[]; TyName_ident="Postgres.smallint"}) -> + list((ot -> match ot with {Int16Array3=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"smallint") + | (1,{TyName_args=[]; TyName_ident="int64"}) -> + list((ot -> match ot with {Int64Array1=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"int64") + | (2,{TyName_args=[]; TyName_ident="int64"}) -> + list((ot -> match ot with {Int64Array2=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"int64") + | (3,{TyName_args=[]; TyName_ident="int64"}) -> + list((ot -> match ot with {Int64Array3=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"int64") + | (1,{TyConst={TyString={}}}) -> + list((ot -> match ot with {StringArray1=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"string") + | (2,{TyConst={TyString={}}}) -> + list((ot -> match ot with {StringArray2=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"string") + | (3,{TyConst={TyString={}}}) -> + list((ot -> match ot with {StringArray3=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"string") + | (1,{TyName_args=[]; TyName_ident="Postgres.real"}) -> + list((ot -> match ot with {RealArray1=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"real") + | (2,{TyName_args=[]; TyName_ident="Postgres.real"}) -> + list((ot -> match ot with {RealArray2=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"real") + | (3,{TyName_args=[]; TyName_ident="Postgres.real"}) -> + list((ot -> match ot with {RealArray3=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"real") + | (1,{TyConst={TyFloat={}}}) -> + list((ot -> match ot with {FloatArray1=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"float") + | (2,{TyConst={TyFloat={}}}) -> + list((ot -> match ot with {FloatArray2=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"float") + | (3,{TyConst={TyFloat={}}}) -> + list((ot -> match ot with {FloatArray3=a} -> {some=@unsafe_cast(a)} | _ -> {none}),"float") + | _ -> + match opatype with + | {Null} -> dflt + | _ -> err("'a") + end + end + and opatype_to_opa(opatype:Postgres.opatype, ty:OpaType.ty, dflt:option('a)): option('a) = match ty with | {TyName_args=[]; TyName_ident="void"} -> (match opatype with | {Null} -> {some=@unsafe_cast(void)} | opatype -> error("expected void, got {opatype}")) + | {TyName_args=[]; TyName_ident="Postgres.smallint"} -> + (match opatype with + | {Null} -> dflt + | {Int16=i} -> + if i < -32768 || i > 32767 + then error("int16 value out of range {i}") + else {some=@unsafe_cast(i)} + | opatype -> error("expected int16, got {opatype}")) | {TyName_args=[]; TyName_ident="int64"} -> (match opatype with | {Null} -> dflt @@ -413,7 +828,9 @@ PostgresTypes = {{ | {Null} -> dflt | {Bool=tf} -> {some=@unsafe_cast(if tf then 1 else 0)} | {Int=i} -> {some=@unsafe_cast(i)} + | {Int16=i} -> {some=@unsafe_cast(i)} | {Int64=i} -> {some=@unsafe_cast(Int64.to_int(i))} + | {Real=f} -> {some=@unsafe_cast(Float.to_int(f))} | {Float=f} -> {some=@unsafe_cast(Float.to_int(f))} | {String=s} -> {some=@unsafe_cast(Int.of_string(s))} | opatype -> error("expected int, got {opatype}")) @@ -422,16 +839,21 @@ PostgresTypes = {{ | {Null} -> {some=@unsafe_cast("")} // Empty strings are returned as Null | {Bool=tf} -> {some=@unsafe_cast(Bool.to_string(tf))} | {Int=i} -> {some=@unsafe_cast(Int.to_string(i))} + | {Int16=i} -> {some=@unsafe_cast(Int.to_string(i))} | {Int64=i} -> {some=@unsafe_cast(Int64.to_string(i))} + | {Real=f} -> {some=@unsafe_cast(Float.to_string(f))} | {Float=f} -> {some=@unsafe_cast(Float.to_string(f))} | {String=s} -> {some=@unsafe_cast(s)} | opatype -> error("expected string, got {opatype}")) + | {TyName_args=[]; TyName_ident="Postgres.real"} | {TyConst={TyFloat={}}} -> (match opatype with | {Null} -> dflt | {Bool=tf} -> {some=@unsafe_cast(if tf then 1.0 else 0.0)} | {Int=i} -> {some=@unsafe_cast(Float.of_int(i))} + | {Int16=i} -> {some=@unsafe_cast(Float.of_int(i))} | {Int64=i} -> {some=@unsafe_cast(Float.of_int(Int64.to_int(i)))} + | {Real=f} -> {some=@unsafe_cast(f)} | {Float=f} -> {some=@unsafe_cast(f)} | {String=s} -> {some=@unsafe_cast(Float.of_string(s))} | opatype -> error("expected float, got {opatype}")) @@ -440,7 +862,9 @@ PostgresTypes = {{ | {Null} -> dflt | {Bool=tf} -> {some=@unsafe_cast(tf)} | {Int=i} -> {some=@unsafe_cast(i != 0)} + | {Int16=i} -> {some=@unsafe_cast(i != 0)} | {Int64=i} -> {some=@unsafe_cast(Int64.op_ne(i,Int64.zero))} + | {Real=d} -> {some=@unsafe_cast(d != 0.0)} | {Float=d} -> {some=@unsafe_cast(d != 0.0)} | {String="true"} | {String="t"} -> {some=@unsafe_cast(true)} | {String="false"} | {String="f"} -> {some=@unsafe_cast(false)} @@ -449,60 +873,33 @@ PostgresTypes = {{ (match opatype with | {Null} -> {some=@unsafe_cast({none})} | _ -> make_option(opatype_to_opa(opatype, ty, none))) - | {TyName_args=[{TyConst={TyInt={}}}]; TyName_ident="list"} -> - (match opatype with - | {Null} -> dflt - | {IntArray1=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(int), got {opatype}")) - | {TyName_args=[{TyName_args=[{TyConst={TyInt={}}}]; TyName_ident="list"}]; TyName_ident="list"} -> - (match opatype with - | {Null} -> dflt - | {IntArray2=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(list(int)), got {opatype}")) - | {TyName_args=[{TyName_args=[{TyName_args=[{TyConst={TyInt={}}}]; TyName_ident="list"}]; TyName_ident="list"}]; TyName_ident="list"} -> - (match opatype with - | {Null} -> dflt - | {IntArray3=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(list(list(int))), got {opatype}")) - | {TyName_args=[{TyConst={TyString={}}}]; TyName_ident="list"} -> - (match opatype with - | {Null} -> dflt - | {StringArray1=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(string), got {opatype}")) - | {TyName_args=[{TyName_args=[{TyConst={TyString={}}}]; TyName_ident="list"}]; TyName_ident="list"} -> - (match opatype with - | {Null} -> dflt - | {StringArray2=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(list(string)), got {opatype}")) - | {TyName_args=[{TyName_args=[{TyName_args=[{TyConst={TyString={}}}]; TyName_ident="list"}]; TyName_ident="list"}]; TyName_ident="list"} -> - (match opatype with - | {Null} -> dflt - | {StringArray3=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(list(list(string))), got {opatype}")) - | {TyName_args=[{TyConst={TyFloat={}}}]; TyName_ident="list"} -> + | {TyName_args=[ty]; TyName_ident="list"} -> opatype_to_list(opatype, ty, dflt, 1) + | {TyName_args=[]; TyName_ident="Postgres.date"} + | {TyName_args=[]; TyName_ident="Date.date"} -> (match opatype with | {Null} -> dflt - | {FloatArray1=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(float), got {opatype}")) - | {TyName_args=[{TyName_args=[{TyConst={TyFloat={}}}]; TyName_ident="list"}]; TyName_ident="list"} -> + | {Date=dt} -> {some=@unsafe_cast(dt)} + | opatype -> error("expected date, got {opatype}")) + | {TyName_args=[]; TyName_ident="Postgres.time"} -> (match opatype with | {Null} -> dflt - | {FloatArray2=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(list(float)), got {opatype}")) - | {TyName_args=[{TyName_args=[{TyName_args=[{TyConst={TyFloat={}}}]; TyName_ident="list"}]; TyName_ident="list"}]; TyName_ident="list"} -> + | {Time=t} -> {some=@unsafe_cast(t)} + | opatype -> error("expected time, got {opatype}")) + | {TyName_args=[]; TyName_ident="Postgres.timestamp"} -> (match opatype with | {Null} -> dflt - | {FloatArray3=ia} -> {some=@unsafe_cast(ia)} - | opatype -> error("expected list(list(list(float))), got {opatype}")) - | {TyName_args=[]; TyName_ident="Date.date"} -> + | {Timestamp=t} -> {some=@unsafe_cast(t)} + | opatype -> error("expected timestamp, got {opatype}")) + | {TyName_args=[]; TyName_ident="Postgres.timestamptz"} -> (match opatype with | {Null} -> dflt - | {Date=dt} -> {some=@unsafe_cast(dt)} - | opatype -> error("expected date, got {opatype}")) + | {Timestamptz=t} -> {some=@unsafe_cast(t)} + | opatype -> error("expected timestamptz, got {opatype}")) + | {TyName_args=[]; TyName_ident="Postgres.bytea"} | {TyName_args=[]; TyName_ident="binary"} -> (match opatype with | {Null} -> dflt - | {Binary=bin} -> {some=@unsafe_cast(bin)} + | {Bytea=bin} -> {some=@unsafe_cast(bin)} | {String=str} -> {some=@unsafe_cast(binary_of_string(str))} | opatype -> error("expected binary, got {opatype}")) | {TyName_args=[]; TyName_ident="Duration.duration"} -> @@ -527,7 +924,7 @@ PostgresTypes = {{ | {Null} -> dflt | {TypeId=(type_id,data)} -> match IntMap.get(type_id,conn.handlers) with - | {some=(hty,handler)} -> + | {some=(_,hty,handler)} -> if ty == hty then match handler(type_id, ty, data) with