1 module pgator.rpc_table;
2 
3 import dpq2.result;
4 import dpq2.oids: OidType;
5 import vibe.core.log;
6 
7 struct Method
8 {
9     string name; // TODO: remove it, AA already contains name of method
10     Statement[] statements;
11     bool isMultiStatement = false;
12     bool readOnlyFlag = false; // TODO: rename to isReadOnly
13     bool needAuthVariablesFlag = false; /// pass username and password from HTTP session to SQL session
14 }
15 
16 struct Statement
17 {
18     short statementNum = -1;
19 
20     // Required parameters:
21     string sqlCommand;
22     string[] argsNames;
23     OidType[] argsOids;
24     string resultName;
25     ResultFormat resultFormat = ResultFormat.TABLE;
26 }
27 
28 enum ResultFormat
29 {
30     TABLE,
31     ROTATED, /// rotate result "counterclockwise"
32     ROW,
33     CELL, /// one cell result
34     VOID /// Run without result (only for multi-statement methods)
35 }
36 
37 struct ReadMethodsResult
38 {
39     Method[string] methods;
40     size_t loaded;
41 }
42 
43 ReadMethodsResult readMethods(immutable Answer answer)
44 {
45     ReadMethodsResult ret;
46 
47     foreach(ref r; rangify(answer))
48     {
49         debug logDebugV("found row: "~r.toString);
50 
51         void getOptionalField(T)(string sqlName, ref T result)
52         {
53             try
54             {
55                 auto v = r[sqlName];
56 
57                 if(v.isNull)
58                     throw new Exception("Value of column "~sqlName~" is NULL", __FILE__, __LINE__);
59 
60                 result = v.as!T;
61             }
62             catch(AnswerException e)
63             {
64                 if(e.type != ExceptionType.COLUMN_NOT_FOUND) throw e;
65             }
66         }
67 
68         Statement s;
69         Method m;
70 
71         // Reading of required parameters
72         try
73         {
74             if(r["method"].isNull)
75                 throw new Exception("Method name is NULL", __FILE__, __LINE__);
76 
77             m.name = r["method"].as!string;
78 
79             if(m.name.length == 0)
80                 throw new Exception("Method name is empty", __FILE__, __LINE__);
81 
82             if(r["sql_query"].isNull)
83                 throw new Exception("sql_query is NULL", __FILE__, __LINE__);
84 
85             s.sqlCommand = r["sql_query"].as!string;
86 
87             if(r["args"].isNull)
88             {
89                 throw new Exception("args[] is NULL", __FILE__, __LINE__);
90             }
91             else
92             {
93                 auto arr = r["args"].asArray;
94 
95                 if(arr.dimsSize.length > 1)
96                     throw new Exception("args[] should be one dimensional", __FILE__, __LINE__);
97 
98                 foreach(ref v; rangify(arr))
99                 {
100                     if(v.isNull || v.as!string.length == 0)
101                         throw new Exception("args[] contains NULL or empty string", __FILE__, __LINE__);
102 
103                     s.argsNames ~= v.as!string;
104                 }
105             }
106         }
107         catch(Exception e)
108         {
109             logFatal(e.msg~", failed on method "~m.name);
110             break;
111         }
112 
113         // Reading of optional parameters
114         try
115         {
116             getOptionalField("read_only", m.readOnlyFlag);
117             getOptionalField("set_auth_variables", m.needAuthVariablesFlag);
118 
119             {
120                 try
121                 {
122                     if(!r["statement_num"].isNull) s.statementNum = r["statement_num"].as!short;
123                 }
124                 catch(AnswerException e)
125                 {
126                     if(e.type != ExceptionType.COLUMN_NOT_FOUND) throw e;
127                 }
128 
129                 if(s.statementNum >= 0)
130                 {
131                     m.isMultiStatement = true;
132                     getOptionalField("result_name", s.resultName);
133                 }
134             }
135 
136             {
137                 string resultFormatStr = "TABLE";
138                 getOptionalField("result_format", resultFormatStr);
139 
140                 switch(resultFormatStr)
141                 {
142                     case "TABLE":
143                         s.resultFormat = ResultFormat.TABLE;
144                         break;
145 
146                     case "ROTATED":
147                         s.resultFormat = ResultFormat.ROTATED;
148                         break;
149 
150                     case "ROW":
151                         s.resultFormat = ResultFormat.ROW;
152                         break;
153 
154                     case "CELL":
155                         s.resultFormat = ResultFormat.CELL;
156                         break;
157 
158                     case "VOID":
159                         if(s.statementNum >= 0)
160                         {
161                             s.resultFormat = ResultFormat.VOID;
162                         }
163                         else
164                         {
165                             throw new Exception("result_format=VOID only for multi-statement transactions", __FILE__, __LINE__);
166                         }
167                         break;
168 
169                     default:
170                         throw new Exception("Unknown result format type "~resultFormatStr, __FILE__, __LINE__);
171                 }
172             }
173         }
174         catch(Exception e)
175         {
176             logWarn("Skipping "~m.name~": "~e.msg);
177             continue;
178         }
179 
180         {
181             auto method = m.name in ret.methods;
182 
183             if(method is null)
184             {
185                 m.statements ~= s;
186                 ret.methods[m.name] = m;
187             }
188             else
189             {
190                 if(s.statementNum < 0)
191                 {
192                     throw new Exception("Duplicate method "~m.name, __FILE__, __LINE__);
193                 }
194                 else // Insert sorted by statementNum
195                 {
196                     import std.array: insertInPlace;
197                     import std.conv: to;
198 
199                     foreach(const i; 0 .. method.statements.length)
200                     {
201                         const storedSNum = method.statements[i].statementNum;
202 
203                         if(storedSNum == s.statementNum)
204                             throw new Exception("Duplicate statement nums "~s.statementNum.to!string~" for method "~m.name, __FILE__, __LINE__);
205 
206                         if(i == method.statements.length - 1)
207                             method.statements ~= s;
208 
209                         if(storedSNum > s.statementNum)
210                         {
211                             method.statements.insertInPlace(i, s);
212                             break;
213                         }
214                     }
215                 }
216             }
217         }
218 
219         ret.loaded++;
220 
221         logDebugV("Method "~m.name~" loaded");
222     }
223 
224     return ret;
225 }