Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: OpenGIS Simple Features Reference Implementation
4 : * Purpose: Implements OGRODBCDataSource class.
5 : * Author: Frank Warmerdam, warmerdam@pobox.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2003, Frank Warmerdam <warmerdam@pobox.com>
9 : * Copyright (c) 2009-2013, Even Rouault <even dot rouault at spatialys.com>
10 : *
11 : * Permission is hereby granted, free of charge, to any person obtaining a
12 : * copy of this software and associated documentation files (the "Software"),
13 : * to deal in the Software without restriction, including without limitation
14 : * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15 : * and/or sell copies of the Software, and to permit persons to whom the
16 : * Software is furnished to do so, subject to the following conditions:
17 : *
18 : * The above copyright notice and this permission notice shall be included
19 : * in all copies or substantial portions of the Software.
20 : *
21 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22 : * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24 : * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 : * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27 : * DEALINGS IN THE SOFTWARE.
28 : ****************************************************************************/
29 :
30 : #include "ogr_odbc.h"
31 : #include "cpl_conv.h"
32 : #include "cpl_string.h"
33 : #include "ogrodbcdrivercore.h"
34 :
35 : /************************************************************************/
36 : /* OGRODBCDataSource() */
37 : /************************************************************************/
38 :
39 2 : OGRODBCDataSource::OGRODBCDataSource()
40 2 : : papoLayers(nullptr), nLayers(0), pszName(nullptr)
41 : {
42 2 : }
43 :
44 : /************************************************************************/
45 : /* ~OGRODBCDataSource() */
46 : /************************************************************************/
47 :
48 4 : OGRODBCDataSource::~OGRODBCDataSource()
49 :
50 : {
51 2 : CPLFree(pszName);
52 :
53 2 : for (int i = 0; i < nLayers; i++)
54 0 : delete papoLayers[i];
55 :
56 2 : CPLFree(papoLayers);
57 4 : }
58 :
59 : /************************************************************************/
60 : /* CheckDSNStringTemplate() */
61 : /* The string will be used as the formatting argument of sprintf with */
62 : /* a string in vararg. So let's check there's only one '%s', and nothing*/
63 : /* else */
64 : /************************************************************************/
65 :
66 0 : static int CheckDSNStringTemplate(const char *pszStr)
67 : {
68 0 : int nPercentSFound = FALSE;
69 0 : while (*pszStr)
70 : {
71 0 : if (*pszStr == '%')
72 : {
73 0 : if (pszStr[1] != 's')
74 : {
75 0 : return FALSE;
76 : }
77 : else
78 : {
79 0 : if (nPercentSFound)
80 0 : return FALSE;
81 0 : nPercentSFound = TRUE;
82 : }
83 : }
84 0 : pszStr++;
85 : }
86 0 : return TRUE;
87 : }
88 :
89 : /************************************************************************/
90 : /* OpenMDB() */
91 : /************************************************************************/
92 :
93 2 : int OGRODBCDataSource::OpenMDB(GDALOpenInfo *poOpenInfo)
94 : {
95 : #ifndef _WIN32
96 : // Try to register MDB Tools driver
97 2 : CPLODBCDriverInstaller::InstallMdbToolsDriver();
98 : #endif /* ndef WIN32 */
99 :
100 2 : const char *pszOptionName = "PGEO_DRIVER_TEMPLATE";
101 : const char *pszDSNStringTemplate =
102 2 : CPLGetConfigOption(pszOptionName, nullptr);
103 2 : if (pszDSNStringTemplate == nullptr)
104 : {
105 2 : pszOptionName = "MDB_DRIVER_TEMPLATE";
106 2 : pszDSNStringTemplate = CPLGetConfigOption(pszOptionName, nullptr);
107 2 : if (pszDSNStringTemplate == nullptr)
108 : {
109 2 : pszOptionName = "";
110 : }
111 : }
112 2 : if (pszDSNStringTemplate && !CheckDSNStringTemplate(pszDSNStringTemplate))
113 : {
114 0 : CPLError(CE_Failure, CPLE_AppDefined, "Illegal value for %s option",
115 : pszOptionName);
116 0 : return FALSE;
117 : }
118 :
119 2 : const char *pszNewName = poOpenInfo->pszFilename;
120 2 : if (!oSession.ConnectToMsAccess(pszNewName, pszDSNStringTemplate))
121 : {
122 2 : return FALSE;
123 : }
124 :
125 : // Retrieve numeric values from MS Access files using ODBC numeric types, to
126 : // avoid loss of precision and missing values on Windows (see
127 : // https://github.com/OSGeo/gdal/issues/3885)
128 0 : m_nStatementFlags |= CPLODBCStatement::Flag::RetrieveNumericColumnsAsDouble;
129 :
130 0 : pszName = CPLStrdup(pszNewName);
131 :
132 : // Collate a list of all tables in the data source
133 0 : CPLODBCStatement oTableList(&oSession);
134 0 : std::vector<CPLString> aosTableNames;
135 0 : if (oTableList.GetTables())
136 : {
137 0 : while (oTableList.Fetch())
138 : {
139 0 : const char *pszSchema = oTableList.GetColData(1);
140 0 : const char *pszTableName = oTableList.GetColData(2);
141 0 : if (pszTableName != nullptr)
142 : {
143 0 : CPLString osLayerName;
144 :
145 0 : if (pszSchema != nullptr && strlen(pszSchema) > 0)
146 : {
147 0 : osLayerName = pszSchema;
148 0 : osLayerName += ".";
149 : }
150 :
151 0 : osLayerName += pszTableName;
152 :
153 0 : const CPLString osLCTableName(CPLString(osLayerName).tolower());
154 0 : m_aosAllLCTableNames.insert(osLCTableName);
155 :
156 0 : aosTableNames.emplace_back(osLayerName);
157 : }
158 : }
159 : }
160 : else
161 : {
162 0 : return FALSE;
163 : }
164 :
165 : /* -------------------------------------------------------------------- */
166 : /* Check if it is a PGeo MDB. */
167 : /* -------------------------------------------------------------------- */
168 0 : for (const CPLString &osTableName : aosTableNames)
169 : {
170 0 : const CPLString osLCTableName(CPLString(osTableName).tolower());
171 0 : if (osLCTableName == "gdb_geomcolumns" /* PGeo */)
172 0 : return FALSE;
173 : }
174 :
175 0 : const bool bListAllTables = CPLTestBool(CSLFetchNameValueDef(
176 0 : poOpenInfo->papszOpenOptions, "LIST_ALL_TABLES", "NO"));
177 :
178 : /* -------------------------------------------------------------------- */
179 : /* Return all tables as non-spatial tables. */
180 : /* -------------------------------------------------------------------- */
181 0 : for (const CPLString &osTableName : aosTableNames)
182 : {
183 0 : const CPLString osLCTableName(CPLString(osTableName).tolower());
184 0 : if (bListAllTables || !(osLCTableName.size() >= 4 &&
185 0 : osLCTableName.substr(0, 4) ==
186 : "msys") // MS Access internal tables
187 : )
188 : {
189 0 : OpenTable(osTableName, nullptr);
190 : }
191 : }
192 :
193 0 : return TRUE;
194 : }
195 :
196 : /************************************************************************/
197 : /* Open() */
198 : /************************************************************************/
199 :
200 2 : int OGRODBCDataSource::Open(GDALOpenInfo *poOpenInfo)
201 : {
202 2 : CPLAssert(nLayers == 0);
203 :
204 2 : const char *pszNewName = poOpenInfo->pszFilename;
205 :
206 2 : constexpr const char *ODBC_PREFIX = "ODBC:";
207 4 : if (!STARTS_WITH_CI(pszNewName, ODBC_PREFIX) &&
208 2 : OGRODBCDriverIsSupportedMsAccessFileExtension(
209 : CPLGetExtension(pszNewName)))
210 2 : return OpenMDB(poOpenInfo);
211 :
212 : /* -------------------------------------------------------------------- */
213 : /* Start parsing dataset name from the end of string, fetching */
214 : /* the name of spatial reference table and names for SRID and */
215 : /* SRTEXT columns first. */
216 : /* -------------------------------------------------------------------- */
217 0 : char *pszWrkName = CPLStrdup(pszNewName + strlen(ODBC_PREFIX));
218 0 : char **papszTables = nullptr;
219 0 : char **papszGeomCol = nullptr;
220 0 : char *pszSRSTableName = nullptr;
221 0 : char *pszSRIDCol = nullptr;
222 0 : char *pszSRTextCol = nullptr;
223 0 : char *pszDelimiter = nullptr;
224 :
225 0 : if ((pszDelimiter = strrchr(pszWrkName, ':')) != nullptr)
226 : {
227 0 : char *pszOBracket = strchr(pszDelimiter + 1, '(');
228 :
229 0 : if (strchr(pszDelimiter, '\\') != nullptr ||
230 0 : strchr(pszDelimiter, '/') != nullptr)
231 : {
232 : /*
233 : ** if there are special tokens then this isn't really
234 : ** the srs table name, so avoid further processing.
235 : */
236 : }
237 0 : else if (pszOBracket == nullptr)
238 : {
239 0 : pszSRSTableName = CPLStrdup(pszDelimiter + 1);
240 0 : *pszDelimiter = '\0';
241 : }
242 : else
243 : {
244 0 : char *pszCBracket = strchr(pszOBracket, ')');
245 0 : if (pszCBracket != nullptr)
246 0 : *pszCBracket = '\0';
247 :
248 0 : char *pszComma = strchr(pszOBracket, ',');
249 0 : if (pszComma != nullptr)
250 : {
251 0 : *pszComma = '\0';
252 0 : pszSRIDCol = CPLStrdup(pszComma + 1);
253 : }
254 :
255 0 : *pszOBracket = '\0';
256 0 : pszSRSTableName = CPLStrdup(pszDelimiter + 1);
257 0 : pszSRTextCol = CPLStrdup(pszOBracket + 1);
258 :
259 0 : *pszDelimiter = '\0';
260 : }
261 : }
262 :
263 : /* -------------------------------------------------------------------- */
264 : /* Strip off any comma delimited set of tables names to access */
265 : /* from the end of the string first. Also allow an optional */
266 : /* bracketed geometry column name after the table name. */
267 : /* -------------------------------------------------------------------- */
268 0 : while ((pszDelimiter = strrchr(pszWrkName, ',')) != nullptr)
269 : {
270 0 : char *pszOBracket = strstr(pszDelimiter + 1, "(");
271 0 : if (pszOBracket == nullptr)
272 : {
273 0 : papszTables = CSLAddString(papszTables, pszDelimiter + 1);
274 0 : papszGeomCol = CSLAddString(papszGeomCol, "");
275 : }
276 : else
277 : {
278 0 : char *pszCBracket = strstr(pszOBracket, ")");
279 :
280 0 : if (pszCBracket != nullptr)
281 0 : *pszCBracket = '\0';
282 :
283 0 : *pszOBracket = '\0';
284 0 : papszTables = CSLAddString(papszTables, pszDelimiter + 1);
285 0 : papszGeomCol = CSLAddString(papszGeomCol, pszOBracket + 1);
286 : }
287 0 : *pszDelimiter = '\0';
288 : }
289 :
290 : /* -------------------------------------------------------------------- */
291 : /* Split out userid, password and DSN. The general form is */
292 : /* user/password@dsn. But if there are no @ characters the */
293 : /* whole thing is assumed to be a DSN. */
294 : /* -------------------------------------------------------------------- */
295 0 : std::string osUserId;
296 0 : std::string osPassword;
297 0 : std::string osDSN;
298 :
299 0 : const char *pszAt = strchr(pszWrkName, '@');
300 0 : if (pszAt == nullptr)
301 : {
302 0 : osDSN = pszWrkName;
303 : }
304 : else
305 : {
306 0 : osDSN = pszAt + 1;
307 0 : osUserId.assign(pszWrkName, pszAt - pszWrkName);
308 0 : const auto nSlashPos = osUserId.find('/');
309 0 : if (nSlashPos != std::string::npos)
310 : {
311 0 : osPassword = osUserId.substr(nSlashPos + 1);
312 0 : osUserId.resize(nSlashPos);
313 : }
314 : }
315 :
316 0 : CPLFree(pszWrkName);
317 :
318 : /* -------------------------------------------------------------------- */
319 : /* Initialize based on the DSN. */
320 : /* -------------------------------------------------------------------- */
321 0 : CPLDebug("OGR_ODBC",
322 : "EstablishSession(DSN:\"%s\", userid:\"%s\", password:\"%s\")",
323 : osDSN.c_str(), osUserId.c_str(), osPassword.c_str());
324 :
325 0 : if (!oSession.EstablishSession(
326 0 : osDSN.c_str(), osUserId.empty() ? nullptr : osUserId.c_str(),
327 0 : osPassword.empty() ? nullptr : osPassword.c_str()))
328 : {
329 0 : CPLError(CE_Failure, CPLE_AppDefined,
330 : "Unable to initialize ODBC connection to DSN for %s,\n"
331 : "%s",
332 : pszNewName + strlen(ODBC_PREFIX), oSession.GetLastError());
333 0 : CSLDestroy(papszTables);
334 0 : CSLDestroy(papszGeomCol);
335 0 : CPLFree(pszSRIDCol);
336 0 : CPLFree(pszSRTextCol);
337 0 : CPLFree(pszSRSTableName);
338 0 : return FALSE;
339 : }
340 :
341 0 : pszName = CPLStrdup(pszNewName);
342 :
343 : /* -------------------------------------------------------------------- */
344 : /* If no explicit list of tables was given, check for a list in */
345 : /* a geometry_columns table. */
346 : /* -------------------------------------------------------------------- */
347 0 : if (papszTables == nullptr)
348 : {
349 0 : CPLODBCStatement oStmt(&oSession);
350 :
351 0 : oStmt.Append("SELECT f_table_name, f_geometry_column, geometry_type"
352 : " FROM geometry_columns");
353 0 : if (oStmt.ExecuteSQL())
354 : {
355 0 : while (oStmt.Fetch())
356 : {
357 0 : papszTables = CSLAddString(papszTables, oStmt.GetColData(0));
358 0 : papszGeomCol = CSLAddString(papszGeomCol, oStmt.GetColData(1));
359 : }
360 : }
361 : }
362 :
363 : /* -------------------------------------------------------------------- */
364 : /* Otherwise our final resort is to return all tables as */
365 : /* non-spatial tables. */
366 : /* -------------------------------------------------------------------- */
367 0 : if (papszTables == nullptr)
368 : {
369 0 : CPLODBCStatement oTableList(&oSession);
370 :
371 0 : if (oTableList.GetTables())
372 : {
373 0 : while (oTableList.Fetch())
374 : {
375 0 : const char *pszSchema = oTableList.GetColData(1);
376 0 : CPLString osLayerName;
377 :
378 0 : if (pszSchema != nullptr && strlen(pszSchema) > 0)
379 : {
380 0 : osLayerName = pszSchema;
381 0 : osLayerName += ".";
382 : }
383 :
384 0 : osLayerName += oTableList.GetColData(2);
385 :
386 0 : papszTables = CSLAddString(papszTables, osLayerName);
387 :
388 0 : papszGeomCol = CSLAddString(papszGeomCol, "");
389 : }
390 : }
391 : }
392 :
393 : /* -------------------------------------------------------------------- */
394 : /* If we have an explicit list of requested tables, use them */
395 : /* (non-spatial). */
396 : /* -------------------------------------------------------------------- */
397 0 : for (int iTable = 0;
398 0 : papszTables != nullptr && papszTables[iTable] != nullptr; iTable++)
399 : {
400 0 : if (strlen(papszGeomCol[iTable]) > 0)
401 0 : OpenTable(papszTables[iTable], papszGeomCol[iTable]);
402 : else
403 0 : OpenTable(papszTables[iTable], nullptr);
404 : }
405 :
406 0 : CSLDestroy(papszTables);
407 0 : CSLDestroy(papszGeomCol);
408 :
409 : #if 0
410 : // NOTE: nothing uses the SRS cache currently. Hence disabled.
411 :
412 : /* -------------------------------------------------------------------- */
413 : /* If no explicit list of tables was given, check for a list in */
414 : /* a geometry_columns table. */
415 : /* -------------------------------------------------------------------- */
416 : if (pszSRSTableName)
417 : {
418 : CPLODBCStatement oSRSList(&oSession);
419 :
420 : if (!pszSRTextCol)
421 : pszSRTextCol = CPLStrdup("srtext");
422 : if (!pszSRIDCol)
423 : pszSRIDCol = CPLStrdup("srid");
424 :
425 : oSRSList.Append("SELECT ");
426 : oSRSList.Append(pszSRIDCol);
427 : oSRSList.Append(",");
428 : oSRSList.Append(pszSRTextCol);
429 : oSRSList.Append(" FROM ");
430 : oSRSList.Append(pszSRSTableName);
431 :
432 : CPLDebug("OGR_ODBC", "ExecuteSQL(%s) to read SRS table",
433 : oSRSList.GetCommand());
434 : if (oSRSList.ExecuteSQL())
435 : {
436 : while (oSRSList.Fetch())
437 : {
438 : const char *pszSRID = oSRSList.GetColData(pszSRIDCol);
439 : if (!pszSRID)
440 : continue;
441 :
442 : const char *pszSRText = oSRSList.GetColData(pszSRTextCol);
443 :
444 : if (pszSRText)
445 : {
446 : std::unique_ptr<OGRSpatialReference, OGRSpatialReferenceReleaser> poSRS(new OGRSpatialReference());
447 : poSRS->SetAxisMappingStrategy(
448 : OAMS_TRADITIONAL_GIS_ORDER);
449 : if (poSRS->importFromWkt(pszSRText) == OGRERR_NONE )
450 : {
451 : m_oSRSCache[atoi(pszSRID)] = std::move(poSRS);
452 : }
453 : }
454 : }
455 : }
456 : }
457 : #endif
458 :
459 0 : if (pszSRIDCol)
460 0 : CPLFree(pszSRIDCol);
461 0 : if (pszSRTextCol)
462 0 : CPLFree(pszSRTextCol);
463 0 : if (pszSRSTableName)
464 0 : CPLFree(pszSRSTableName);
465 :
466 0 : return TRUE;
467 : }
468 :
469 : /************************************************************************/
470 : /* OpenTable() */
471 : /************************************************************************/
472 :
473 0 : int OGRODBCDataSource::OpenTable(const char *pszNewName, const char *pszGeomCol)
474 : {
475 : /* -------------------------------------------------------------------- */
476 : /* Create the layer object. */
477 : /* -------------------------------------------------------------------- */
478 0 : OGRODBCTableLayer *poLayer = new OGRODBCTableLayer(this, m_nStatementFlags);
479 :
480 0 : if (poLayer->Initialize(pszNewName, pszGeomCol))
481 : {
482 0 : delete poLayer;
483 0 : return FALSE;
484 : }
485 :
486 : /* -------------------------------------------------------------------- */
487 : /* Add layer to data source layer list. */
488 : /* -------------------------------------------------------------------- */
489 0 : papoLayers = (OGRODBCLayer **)CPLRealloc(
490 0 : papoLayers, sizeof(OGRODBCLayer *) * (nLayers + 1));
491 0 : papoLayers[nLayers++] = poLayer;
492 :
493 0 : return TRUE;
494 : }
495 :
496 : /************************************************************************/
497 : /* TestCapability() */
498 : /************************************************************************/
499 :
500 0 : int OGRODBCDataSource::TestCapability(CPL_UNUSED const char *pszCap)
501 : {
502 0 : return FALSE;
503 : }
504 :
505 : /************************************************************************/
506 : /* GetLayer() */
507 : /************************************************************************/
508 :
509 0 : OGRLayer *OGRODBCDataSource::GetLayer(int iLayer)
510 :
511 : {
512 0 : if (iLayer < 0 || iLayer >= nLayers)
513 0 : return nullptr;
514 : else
515 0 : return papoLayers[iLayer];
516 : }
517 :
518 : /************************************************************************/
519 : /* GetLayerByName() */
520 : /************************************************************************/
521 :
522 0 : OGRLayer *OGRODBCDataSource::GetLayerByName(const char *pszLayerName)
523 : {
524 0 : OGRLayer *poLayer = GDALDataset::GetLayerByName(pszLayerName);
525 0 : if (poLayer != nullptr)
526 0 : return poLayer;
527 :
528 : // if table name doesn't exist in database, don't try any further
529 0 : const CPLString osLCTableName(CPLString(pszLayerName).tolower());
530 0 : if (m_aosAllLCTableNames.find(osLCTableName) == m_aosAllLCTableNames.end())
531 0 : return nullptr;
532 :
533 : // try to open the table -- if successful the table will be added to
534 : // papoLayers as the last item
535 0 : if (OpenTable(pszLayerName, nullptr))
536 0 : return papoLayers[nLayers - 1];
537 : else
538 0 : return nullptr;
539 : }
540 :
541 : /************************************************************************/
542 : /* IsPrivateLayerName() */
543 : /************************************************************************/
544 :
545 0 : bool OGRODBCDataSource::IsPrivateLayerName(const CPLString &osName)
546 : {
547 0 : const CPLString osLCTableName(CPLString(osName).tolower());
548 :
549 0 : return osLCTableName.size() >= 4 &&
550 0 : osLCTableName.substr(0, 4) == "msys"; // MS Access internal tables
551 : }
552 :
553 : /************************************************************************/
554 : /* IsLayerPrivate() */
555 : /************************************************************************/
556 :
557 0 : bool OGRODBCDataSource::IsLayerPrivate(int iLayer) const
558 : {
559 0 : if (iLayer < 0 || iLayer >= nLayers)
560 0 : return false;
561 :
562 0 : const std::string osName(papoLayers[iLayer]->GetName());
563 0 : return IsPrivateLayerName(osName);
564 : }
565 :
566 : /************************************************************************/
567 : /* ExecuteSQL() */
568 : /************************************************************************/
569 :
570 0 : OGRLayer *OGRODBCDataSource::ExecuteSQL(const char *pszSQLCommand,
571 : OGRGeometry *poSpatialFilter,
572 : const char *pszDialect)
573 :
574 : {
575 : /* -------------------------------------------------------------------- */
576 : /* Use generic implementation for recognized dialects */
577 : /* -------------------------------------------------------------------- */
578 0 : if (IsGenericSQLDialect(pszDialect))
579 0 : return OGRDataSource::ExecuteSQL(pszSQLCommand, poSpatialFilter,
580 0 : pszDialect);
581 :
582 : /* -------------------------------------------------------------------- */
583 : /* Execute statement. */
584 : /* -------------------------------------------------------------------- */
585 : CPLODBCStatement *poStmt =
586 0 : new CPLODBCStatement(&oSession, m_nStatementFlags);
587 :
588 0 : CPLDebug("ODBC", "ExecuteSQL(%s) called.", pszSQLCommand);
589 0 : poStmt->Append(pszSQLCommand);
590 0 : if (!poStmt->ExecuteSQL())
591 : {
592 0 : CPLError(CE_Failure, CPLE_AppDefined, "%s", oSession.GetLastError());
593 0 : delete poStmt;
594 0 : return nullptr;
595 : }
596 :
597 : /* -------------------------------------------------------------------- */
598 : /* Are there result columns for this statement? */
599 : /* -------------------------------------------------------------------- */
600 0 : if (poStmt->GetColCount() == 0)
601 : {
602 0 : delete poStmt;
603 0 : CPLErrorReset();
604 0 : return nullptr;
605 : }
606 :
607 : /* -------------------------------------------------------------------- */
608 : /* Create a results layer. It will take ownership of the */
609 : /* statement. */
610 : /* -------------------------------------------------------------------- */
611 :
612 0 : OGRODBCSelectLayer *poLayer = new OGRODBCSelectLayer(this, poStmt);
613 :
614 0 : if (poSpatialFilter != nullptr)
615 0 : poLayer->SetSpatialFilter(poSpatialFilter);
616 :
617 0 : return poLayer;
618 : }
619 :
620 : /************************************************************************/
621 : /* ReleaseResultSet() */
622 : /************************************************************************/
623 :
624 0 : void OGRODBCDataSource::ReleaseResultSet(OGRLayer *poLayer)
625 :
626 : {
627 0 : delete poLayer;
628 0 : }
|