1 | /**********************************************************************
|
---|
2 | *
|
---|
3 | * mssqldbclass.cpp --
|
---|
4 | * Copyright (C) 2008 DL Consulting Ltd
|
---|
5 | *
|
---|
6 | * A component of the Greenstone digital library software
|
---|
7 | * from the New Zealand Digital Library Project at the
|
---|
8 | * University of Waikato, New Zealand.
|
---|
9 | *
|
---|
10 | * This program is free software; you can redistribute it and/or modify
|
---|
11 | * it under the terms of the GNU General Public License as published by
|
---|
12 | * the Free Software Foundation; either version 2 of the License, or
|
---|
13 | * (at your option) any later version.
|
---|
14 | *
|
---|
15 | * This program is distributed in the hope that it will be useful,
|
---|
16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
---|
18 | * GNU General Public License for more details.
|
---|
19 | *
|
---|
20 | * You should have received a copy of the GNU General Public License
|
---|
21 | * along with this program; if not, write to the Free Software
|
---|
22 | * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
---|
23 | *
|
---|
24 | *********************************************************************/
|
---|
25 |
|
---|
26 | #include "mssqldbclass.h"
|
---|
27 | #include "gsdlunicode.h"
|
---|
28 | #include "unitool.h"
|
---|
29 | #include "cfgread.h"
|
---|
30 | #include <stdio.h>
|
---|
31 |
|
---|
32 | #ifdef __WIN32__
|
---|
33 | // for Sleep
|
---|
34 | # include <windows.h>
|
---|
35 | #else
|
---|
36 | // for usleep
|
---|
37 | # include <unistd.h>
|
---|
38 | #endif
|
---|
39 |
|
---|
40 |
|
---|
41 | #define MSSQL_MAX_RETRIES 8
|
---|
42 |
|
---|
43 |
|
---|
44 | mssqldbclass::mssqldbclass (const text_t& gsdlhome)
|
---|
45 | : sqldbclass(gsdlhome)
|
---|
46 | {
|
---|
47 | dbptr = NULL;
|
---|
48 | tableid = "";
|
---|
49 | debug = true;
|
---|
50 | }
|
---|
51 |
|
---|
52 |
|
---|
53 | mssqldbclass::~mssqldbclass ()
|
---|
54 | {
|
---|
55 | closedatabase();
|
---|
56 | }
|
---|
57 |
|
---|
58 |
|
---|
59 | // returns true if opened
|
---|
60 | bool mssqldbclass::opendatabase (const text_t &filename, int mode, int num_retrys,
|
---|
61 | #ifdef __WIN32__
|
---|
62 | bool need_filelock
|
---|
63 | #else
|
---|
64 | bool
|
---|
65 | #endif
|
---|
66 | )
|
---|
67 | {
|
---|
68 | // Read the database information in from the .mssqldbinfo file
|
---|
69 | debug_output("MSSQL: Read in infodb file:[" + filename + "]\n");
|
---|
70 | text_tmap db_info_map = read_infodb_file(filename);
|
---|
71 | tableid = db_info_map["tableid"];
|
---|
72 |
|
---|
73 | // Initializing connection to MS-SQL
|
---|
74 | debug_output("MSSQL: Opening connection...\n");
|
---|
75 | CoInitialize(NULL);
|
---|
76 | char *db;
|
---|
77 | text_t constr = "Provider=SQLNCLI;DataTypeCompatibility=80;Server=" + db_info_map["mss-host"] + ";User ID=" + db_info_map["username"] + ";Password=" + db_info_map["password"] + ";Initial Catalog=" + db_info_map["database"] + ";AutoTranslate=on;";
|
---|
78 | db = constr.getcstr();
|
---|
79 |
|
---|
80 | // Handle the error when create connection instance failed
|
---|
81 | if (FAILED(dbptr.CreateInstance(__uuidof(Connection))))
|
---|
82 | {
|
---|
83 | cerr << "mssqldbclass::opendatabase(): CreateInstance failed." << endl;
|
---|
84 | return false;
|
---|
85 | }
|
---|
86 |
|
---|
87 | // Try to open the database, if it failed, output the error.
|
---|
88 | // Note, the error description is not really helpful, but you can google the
|
---|
89 | // error number and you can always find something helpful
|
---|
90 | try
|
---|
91 | {
|
---|
92 | dbptr->Open(db, "", "", -1);
|
---|
93 | }
|
---|
94 | catch (_com_error &e)
|
---|
95 | {
|
---|
96 | text_t error;
|
---|
97 | error.setcstr(e.ErrorMessage());
|
---|
98 | cerr << "mssqldbclass::opendatabase(): Open failed (" << error << ")" << endl;
|
---|
99 | }
|
---|
100 | catch (...)
|
---|
101 | {
|
---|
102 | cerr << "mssqldbclass::opendatabase(): Open failed badly." << endl;
|
---|
103 | }
|
---|
104 |
|
---|
105 | CoUninitialize();
|
---|
106 |
|
---|
107 | return true;
|
---|
108 | }
|
---|
109 |
|
---|
110 |
|
---|
111 | void mssqldbclass::closedatabase ()
|
---|
112 | {
|
---|
113 | // Close the database and catch any error if failed.
|
---|
114 | if (dbptr != NULL)
|
---|
115 | {
|
---|
116 | debug_output("MSSQL: Closing Database\n");
|
---|
117 | try
|
---|
118 | {
|
---|
119 | dbptr->Close();
|
---|
120 | }
|
---|
121 | catch (_com_error &e)
|
---|
122 | {
|
---|
123 | text_t error;
|
---|
124 | error.setcstr(e.ErrorMessage());
|
---|
125 | cerr << "mssqldbclass::closedatabase(): Close failed (" << error << ")" << endl;
|
---|
126 | }
|
---|
127 | catch (...)
|
---|
128 | {
|
---|
129 | cerr << "mssqldbclass::closedatabase(): Close failed badly." << endl;
|
---|
130 | }
|
---|
131 | delete dbptr;
|
---|
132 | dbptr = NULL;
|
---|
133 | }
|
---|
134 | }
|
---|
135 |
|
---|
136 |
|
---|
137 | // Return the file extension for the mssql's info file
|
---|
138 | text_t mssqldbclass::getfileextension ()
|
---|
139 | {
|
---|
140 | return ".mssqldbinfo";
|
---|
141 | }
|
---|
142 |
|
---|
143 |
|
---|
144 | void mssqldbclass::deletekey (const text_t &key)
|
---|
145 | {
|
---|
146 | text_t sql_cmd = "DELETE FROM data_" + tableid + " WHERE one_key=N'" + sql_safe(key) + "'";
|
---|
147 | sqlexec(sql_cmd);
|
---|
148 | }
|
---|
149 |
|
---|
150 |
|
---|
151 | // returns true on success
|
---|
152 | bool mssqldbclass::getkeydata (const text_t& key, text_t &data)
|
---|
153 | {
|
---|
154 | text_t sql_cmd = "SELECT one_value FROM data_" + tableid + " WHERE one_key=N'" + sql_safe(key) + "'";
|
---|
155 | vector<text_tmap> sql_results;
|
---|
156 | if (!sqlgetarray(sql_cmd, sql_results) || sql_results.size() == 0)
|
---|
157 | {
|
---|
158 | return false;
|
---|
159 | }
|
---|
160 |
|
---|
161 | text_tmap sql_result = sql_results[0];
|
---|
162 | data = sql_result["one_value"];
|
---|
163 | return true;
|
---|
164 | }
|
---|
165 |
|
---|
166 |
|
---|
167 | // returns array of keys
|
---|
168 | text_tarray mssqldbclass::getkeys ()
|
---|
169 | {
|
---|
170 | text_tarray keys;
|
---|
171 |
|
---|
172 | // Get all the entries in the "key" column of the table
|
---|
173 | text_t sql_cmd = "SELECT one_key FROM data_" + tableid;
|
---|
174 | vector<text_tmap> sql_results;
|
---|
175 | if (!sqlgetarray(sql_cmd, sql_results) || sql_results.size() == 0)
|
---|
176 | {
|
---|
177 | return keys;
|
---|
178 | }
|
---|
179 |
|
---|
180 | // Iterate through the keys and add them to the array to be returned
|
---|
181 | vector<text_tmap>::iterator sql_results_iterator = sql_results.begin();
|
---|
182 | while (sql_results_iterator != sql_results.end())
|
---|
183 | {
|
---|
184 | text_tmap sql_result = (*sql_results_iterator);
|
---|
185 | keys.push_back(sql_result["one_key"]);
|
---|
186 | sql_results_iterator++;
|
---|
187 | }
|
---|
188 |
|
---|
189 | return keys;
|
---|
190 | }
|
---|
191 |
|
---|
192 |
|
---|
193 | // returns true on success
|
---|
194 | bool mssqldbclass::setkeydata (const text_t &key, const text_t &data)
|
---|
195 | {
|
---|
196 | // We need to do either an INSERT or UPDATE depending on whether the key already exists
|
---|
197 | if (!exists(key))
|
---|
198 | {
|
---|
199 | text_t sql_cmd = "INSERT INTO data_" + tableid + " (one_key, one_value) VALUES (N'" + sql_safe(key) + "', N'" + sql_safe(data) + "')";
|
---|
200 | return sqlexec(sql_cmd);
|
---|
201 | }
|
---|
202 | else
|
---|
203 | {
|
---|
204 | text_t sql_cmd = "UPDATE data_" + tableid + " SET one_value='" + sql_safe(data) + "' WHERE one_key=N'" + sql_safe(key) + "'";
|
---|
205 | return sqlexec(sql_cmd);
|
---|
206 | }
|
---|
207 | }
|
---|
208 |
|
---|
209 |
|
---|
210 | // returns array of document OIDs
|
---|
211 | text_tarray mssqldbclass::get_documents_with_metadata_value (const text_tarray &metadata_element_names,
|
---|
212 | const text_t &metadata_value,
|
---|
213 | const text_t &sort_by_metadata_element_name)
|
---|
214 | {
|
---|
215 | text_tarray document_OIDs;
|
---|
216 |
|
---|
217 | // Check at least one metadata element and a metadata value has been specified
|
---|
218 | if (metadata_element_names.empty() || metadata_value == "")
|
---|
219 | {
|
---|
220 | return document_OIDs;
|
---|
221 | }
|
---|
222 |
|
---|
223 | // Get the entries in the "document_metadata" table where the element and value matches those specified
|
---|
224 | text_t sql_cmd = "SELECT DISTINCT docOID FROM document_metadata_" + tableid + " WHERE element IN (N'" + sql_safe(metadata_element_names[0]) + "'";
|
---|
225 | for (int i = 1; i < metadata_element_names.size(); i++)
|
---|
226 | {
|
---|
227 | sql_cmd += ",'" + sql_safe(metadata_element_names[i]) + "'";
|
---|
228 | }
|
---|
229 | sql_cmd += ") AND value='" + sql_safe(metadata_value) + "'";
|
---|
230 |
|
---|
231 | // If we're sorting the documents by a certain metadata element, extend the SQL command to do this
|
---|
232 | if (sort_by_metadata_element_name != "")
|
---|
233 | {
|
---|
234 | sql_cmd = "SELECT docOID FROM (" + sql_cmd + ") LEFT JOIN (SELECT docOID,value from document_metadata_" + tableid + " WHERE element=N'" + sql_safe(sort_by_metadata_element_name) + "') USING (docOID) ORDER by value";
|
---|
235 | }
|
---|
236 |
|
---|
237 | // Perform the SQL request
|
---|
238 | vector<text_tmap> sql_results;
|
---|
239 | if (!sqlgetarray(sql_cmd, sql_results) || sql_results.size() == 0)
|
---|
240 | {
|
---|
241 | return document_OIDs;
|
---|
242 | }
|
---|
243 |
|
---|
244 | // Iterate through the documents and add them to the array to be returned
|
---|
245 | vector<text_tmap>::iterator sql_results_iterator = sql_results.begin();
|
---|
246 | while (sql_results_iterator != sql_results.end())
|
---|
247 | {
|
---|
248 | text_tmap sql_result = (*sql_results_iterator);
|
---|
249 | document_OIDs.push_back(sql_result["docOID"]);
|
---|
250 | sql_results_iterator++;
|
---|
251 | }
|
---|
252 |
|
---|
253 | return document_OIDs;
|
---|
254 | }
|
---|
255 |
|
---|
256 |
|
---|
257 | // returns array of values
|
---|
258 | text_tarray mssqldbclass::get_metadata_values (const text_tarray &metadata_element_names,
|
---|
259 | const text_t &metadata_value_filter,
|
---|
260 | const text_t &metadata_value_grouping_expression)
|
---|
261 | {
|
---|
262 | text_tarray metadata_values;
|
---|
263 |
|
---|
264 | // Check at least one metadata element has been specified
|
---|
265 | if (metadata_element_names.empty())
|
---|
266 | {
|
---|
267 | return metadata_values;
|
---|
268 | }
|
---|
269 |
|
---|
270 | // Get the raw "value" field unless a grouping expression was provided (in this case an edited value is returned)
|
---|
271 | text_t value_select_expression = "value";
|
---|
272 | if (metadata_value_grouping_expression != "")
|
---|
273 | {
|
---|
274 | value_select_expression = metadata_value_grouping_expression;
|
---|
275 | }
|
---|
276 |
|
---|
277 | // Get the entries in the "document_metadata" table where the element matches that specified
|
---|
278 | text_t sql_cmd = "SELECT DISTINCT docOID," + value_select_expression + " FROM document_metadata_" + tableid + " WHERE element IN (N'" + sql_safe(metadata_element_names[0]) + "'";
|
---|
279 | for (int i = 1; i < metadata_element_names.size(); i++)
|
---|
280 | {
|
---|
281 | sql_cmd += ",N'" + sql_safe(metadata_element_names[i]) + "'";
|
---|
282 | }
|
---|
283 | sql_cmd += ")";
|
---|
284 |
|
---|
285 | // Add value filter, if one has been defined
|
---|
286 | if (metadata_value_filter != "")
|
---|
287 | {
|
---|
288 | sql_cmd += " AND value GLOB N'" + sql_safe(metadata_value_filter) + "'";
|
---|
289 | }
|
---|
290 |
|
---|
291 | // Perform the SQL request
|
---|
292 | vector<text_tmap> sql_results;
|
---|
293 | if (!sqlgetarray(sql_cmd, sql_results) || sql_results.size() == 0)
|
---|
294 | {
|
---|
295 | return metadata_values;
|
---|
296 | }
|
---|
297 |
|
---|
298 | // Iterate through the values and add them to the array to be returned
|
---|
299 | vector<text_tmap>::iterator sql_results_iterator = sql_results.begin();
|
---|
300 | while (sql_results_iterator != sql_results.end())
|
---|
301 | {
|
---|
302 | text_tmap sql_result = (*sql_results_iterator);
|
---|
303 | metadata_values.push_back(sql_result[value_select_expression]);
|
---|
304 | sql_results_iterator++;
|
---|
305 | }
|
---|
306 |
|
---|
307 | return metadata_values;
|
---|
308 | }
|
---|
309 |
|
---|
310 |
|
---|
311 | // sqlexec(const text_t &sql)
|
---|
312 | // Takes a sql statement and executes it
|
---|
313 | // Returns false if failed, otherwise true
|
---|
314 | bool mssqldbclass::sqlexec (const text_t &sql)
|
---|
315 | {
|
---|
316 | char *sql_c = sql.getcstr();
|
---|
317 | bool rv = true;
|
---|
318 |
|
---|
319 | // Creating the command instance
|
---|
320 | _CommandPtr cmd;
|
---|
321 | if (FAILED(cmd.CreateInstance(__uuidof(Command))))
|
---|
322 | {
|
---|
323 | cerr << "mssqldbclass::sqlexec: CreateInstance failed" << endl;
|
---|
324 | return false;
|
---|
325 | }
|
---|
326 |
|
---|
327 | // Execute the sql statements and catch any error encountered
|
---|
328 | try
|
---|
329 | {
|
---|
330 | cmd->ActiveConnection = dbptr;
|
---|
331 | cmd->CommandText = sql_c;
|
---|
332 | cmd->Execute(NULL, NULL, adCmdText);
|
---|
333 | }
|
---|
334 | catch (_com_error &e)
|
---|
335 | {
|
---|
336 | text_t error;
|
---|
337 | error.setcstr(e.ErrorMessage());
|
---|
338 | cerr << "mssqldbclass::sqlexec: _com_error: (" << sql << ") : (" << error << ")" << endl;
|
---|
339 | rv = false;
|
---|
340 | }
|
---|
341 | catch (...)
|
---|
342 | {
|
---|
343 | cerr << "mssqldbclass::sqlexec: unknown error: (" + sql + ")" << endl;
|
---|
344 | rv = false;
|
---|
345 | }
|
---|
346 |
|
---|
347 | delete [] sql_c;
|
---|
348 |
|
---|
349 | return rv;
|
---|
350 | }
|
---|
351 |
|
---|
352 |
|
---|
353 | // sqlgetarray(const text_t &sql_cmd, vector<text_tmap> &sql_results)
|
---|
354 | // Takes a sql statement and returns a vetor of result text_tmaps
|
---|
355 | // Each map containing [field] => [value]. Note, all in text_t
|
---|
356 | bool mssqldbclass::sqlgetarray (const text_t &sql_cmd,
|
---|
357 | vector<text_tmap> &sql_results)
|
---|
358 | {
|
---|
359 | _RecordsetPtr rset;
|
---|
360 | bool result = false;
|
---|
361 | char *sql_c = sql_cmd.getcstr();
|
---|
362 |
|
---|
363 | // Create instance of the Recordset and die if failed
|
---|
364 | if (FAILED(rset.CreateInstance(__uuidof(Recordset))))
|
---|
365 | {
|
---|
366 | cerr << "mssqldbclass::sqlgetarray(): CreateInstance failed." << endl;
|
---|
367 | return false;
|
---|
368 | }
|
---|
369 |
|
---|
370 | // Do the query to the database - this can throw exceptions, hence
|
---|
371 | // the try...catch
|
---|
372 | try
|
---|
373 | {
|
---|
374 | rset->Open(sql_c, dbptr.GetInterfacePtr(), adOpenStatic, adLockReadOnly, adCmdText);
|
---|
375 |
|
---|
376 | // There may have been no results found
|
---|
377 | if (rset->RecordCount < 1)
|
---|
378 | {
|
---|
379 | debug_output("mssqldbclass::sqlgetarray(): No records found!\n");
|
---|
380 | return true; // Still want to return true even if there is no result found
|
---|
381 | }
|
---|
382 |
|
---|
383 | // Retrieve the informaton and place it into the sql_result map vector
|
---|
384 | // It will be array[["key"=>"values"]]
|
---|
385 | while(!rset->EndOfFile)
|
---|
386 | {
|
---|
387 | text_tmap rowmap;
|
---|
388 | _variant_t index;
|
---|
389 | index.vt = VT_I2;
|
---|
390 | for (int i = 0; i < rset->Fields->Count; i++)
|
---|
391 | {
|
---|
392 | index.iVal = i;
|
---|
393 | text_t v = "";
|
---|
394 | text_t n = (char *)_bstr_t(rset->Fields->GetItem(index)->Name);
|
---|
395 | if (rset->Fields->GetItem(index)->Value.vt != VT_NULL)
|
---|
396 | {
|
---|
397 | v = convert_bstr_to_textt(rset->Fields->GetItem(index)->Value);
|
---|
398 | }
|
---|
399 | rowmap[n] = v;
|
---|
400 | }
|
---|
401 | sql_results.push_back(rowmap);
|
---|
402 | rset->MoveNext();
|
---|
403 | }
|
---|
404 | result = true;
|
---|
405 | }
|
---|
406 | catch (_com_error &e)
|
---|
407 | {
|
---|
408 | text_t error;
|
---|
409 | error.setcstr(e.ErrorMessage());
|
---|
410 | cerr << "mssqldbclass::sqlgetarray(): Error " << error << endl;
|
---|
411 | result = false;
|
---|
412 | }
|
---|
413 | catch (...)
|
---|
414 | {
|
---|
415 | cerr << "mssqldbclass::sqlgetarray(): Unknown Error" << endl;
|
---|
416 | result = false;
|
---|
417 | }
|
---|
418 |
|
---|
419 | delete [] sql_c;
|
---|
420 |
|
---|
421 | return result;
|
---|
422 | }
|
---|
423 |
|
---|
424 |
|
---|
425 | // returns true if exists
|
---|
426 | bool sqlitedbclass::sqltableexists(const text_t &table_name)
|
---|
427 | {
|
---|
428 | cerr << "**** mssqldbclass::sqltableexists: This implementation is untested!" << endl;
|
---|
429 | cerr << "**** Remove warning message once confirmed/fixed to works as required" << endl;
|
---|
430 |
|
---|
431 | text_t sql_cmd = "SELECT name FROM sysobjects WHERE name='" + sql_safe(table_name)
|
---|
432 | + "' AND OBJECTPROPERTY(id,'IsUserTable')=1";
|
---|
433 |
|
---|
434 | vector<text_tmap> sql_results;
|
---|
435 | if (!sqlgetarray(sql_cmd, sql_results) || sql_results.size() == 0)
|
---|
436 | {
|
---|
437 | return false;
|
---|
438 | }
|
---|
439 |
|
---|
440 | return true;
|
---|
441 | }
|
---|
442 |
|
---|
443 |
|
---|
444 | text_t mssqldbclass::sql_safe (const text_t &value_arg)
|
---|
445 | {
|
---|
446 | text_t value = value_arg;
|
---|
447 | value.replace("'", "''");
|
---|
448 | return value;
|
---|
449 | }
|
---|
450 |
|
---|
451 |
|
---|
452 | //---------------------------------------------------------------------------//
|
---|
453 | // MS-SQL Private Utilities [START]
|
---|
454 | //---------------------------------------------------------------------------//
|
---|
455 |
|
---|
456 |
|
---|
457 | // read_infodb_file(text_t filename)
|
---|
458 | // Unlike the others (sqlite and gdbm), this is a server-client based
|
---|
459 | // database. Therefore, instead of store the actual database, we store the
|
---|
460 | // infoserver-client based database.
|
---|
461 | // What we want to do here is to have the
|
---|
462 | text_tmap mssqldbclass::read_infodb_file(text_t filename)
|
---|
463 | {
|
---|
464 | text_tarray cfgline;
|
---|
465 | text_tmap dbinfo_hash;
|
---|
466 |
|
---|
467 | // Read in the infodb file
|
---|
468 | // It is the same format as all other config file
|
---|
469 | // Therefore let's reuse the confin code
|
---|
470 | char *infodb_cfgc = filename.getcstr();
|
---|
471 | ifstream confin(infodb_cfgc);
|
---|
472 |
|
---|
473 | // Read in the config and store it into the hash
|
---|
474 | if (confin)
|
---|
475 | {
|
---|
476 | while (read_cfg_line(confin, cfgline) >= 0)
|
---|
477 | {
|
---|
478 | if (cfgline.size() == 2)
|
---|
479 | {
|
---|
480 | dbinfo_hash[cfgline[0]] = cfgline[1];
|
---|
481 | }
|
---|
482 | }
|
---|
483 | confin.close();
|
---|
484 | }
|
---|
485 | delete []infodb_cfgc;
|
---|
486 | return dbinfo_hash;
|
---|
487 | }
|
---|
488 |
|
---|
489 |
|
---|
490 | void mssqldbclass::debug_output (text_t output_string)
|
---|
491 | {
|
---|
492 | // Only ouput this when we want to see the debug message
|
---|
493 | if (debug)
|
---|
494 | {
|
---|
495 | cerr << output_string;
|
---|
496 | }
|
---|
497 | }
|
---|
498 |
|
---|
499 |
|
---|
500 | text_t mssqldbclass::convert_bstr_to_textt (_variant_t variant)
|
---|
501 | {
|
---|
502 | text_t result = "";
|
---|
503 |
|
---|
504 | // For some reasons Windows Server 2003 does not
|
---|
505 | // allow _bstr_t to be casted to unsigned short* directly
|
---|
506 | // Therefore we need to convert it to wchar_t * then convert
|
---|
507 | // to unsigned short*
|
---|
508 | _bstr_t in = _bstr_t(variant);
|
---|
509 | wchar_t* temp_conv = (wchar_t*)in;
|
---|
510 | unsigned short* ints = (unsigned short*)temp_conv;
|
---|
511 | for (int i = 0 ; i < in.length() ; i++)
|
---|
512 | {
|
---|
513 | result.push_back(ints[i]);
|
---|
514 | }
|
---|
515 | return result;
|
---|
516 | }
|
---|
517 |
|
---|
518 | //---------------------------------------------------------------------------//
|
---|
519 | // MS-SQL Private Utilities [END]
|
---|
520 | //---------------------------------------------------------------------------//
|
---|
521 |
|
---|