/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/Model/postgisdb.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/Model/postgisdb.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1946 - (show annotations)
Thu Nov 13 18:56:41 2003 UTC (21 years, 4 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 15004 byte(s)
Quote table and column names properly for postgis.

(quote_identifier): New. Function to
quote an identifier for use in an sql statement
(PostGISColumn.__init__): Add the quoted_name attribute
(PostGISTable.__init__): New instance variable quoted_tablename
(PostGISTable._fetch_table_information): Use the quoted table
name. New isntance variable quoted_geo_col with a quoted version
of geometry_column
(PostGISTable.NumRows, PostGISTable.RowIdToOrdinal)
(PostGISTable.RowOrdinalToId): Use the quoted table name
(PostGISTable.ReadValue, PostGISTable.ValueRange)
(PostGISTable.UniqueValues, PostGISTable.SimpleQuery)
(PostGISShapeStore.BoundingBox, PostGISShapeStore.Shape)
(PostGISShapeStore.AllShapes, PostGISShapeStore.ShapesInRegion):
Use quoted table and column names

1 # Copyright (C) 2003 by Intevation GmbH
2 # Authors:
3 # Martin Mueller <[email protected]>
4 # Bernhard Herzog <[email protected]>
5 #
6 # This program is free software under the GPL (>=v2)
7 # Read the file COPYING coming with the software for details.
8
9 """Basic interface to a PostGIS database"""
10
11 from __future__ import generators
12
13 try:
14 import psycopg
15 except ImportError:
16 psycopg = None
17
18 import table
19 import wellknowntext
20
21 from data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, SHAPETYPE_POINT, RAW_WKT
22
23 def has_postgis_support():
24 """Return whether this Thuban instance supports PostGIS connections
25
26 Having PostGIS support means that the psycopg module can be
27 imported.
28 """
29 return psycopg is not None
30
31 def psycopg_version():
32 return psycopg.__version__
33
34 if psycopg is not None:
35 type_map = [(psycopg.STRING, table.FIELDTYPE_STRING),
36 (psycopg.INTEGER, table.FIELDTYPE_INT),
37 (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
38
39
40 def quote_identifier(ident):
41 """Return a quoted version of the identifier ident.
42
43 The return value is a string that can be put directly into an SQL
44 statement. The quoted identifier is surrounded by double quotes and
45 any double quotes alread in the input value are converted to two
46 double quotes. Examples:
47
48 >>> quote_identifier("abc\"def")
49 '"abc""def"'
50 >>> quote_identifier("abc def")
51 '"abc def"'
52 """
53 return '"' + '""'.join(ident.split('"')) + '"'
54
55
56 class ConnectionError(Exception):
57
58 """Class for exceptions occurring when establishing a Databse connection"""
59
60
61 class PostGISConnection:
62
63 """Represent a PostGIS database
64
65 A PostGISConnection instance has the following public attributes:
66
67 dbname -- The name of the database
68 host, port -- Host and port to connect to
69 user -- The user name to connect as.
70
71 All of these attributes are strings and may be empty strings to
72 indicate default values.
73 """
74
75 def __init__(self, dbname, host="", user="", password="", dbtype="",
76 port=""):
77 self.dbname = dbname
78 self.host = host
79 self.port = port
80 self.user = user
81 self.password = password
82 self.dbtype = dbtype
83 self.connect()
84
85 def connect(self):
86 """Internal: Establish the database connection"""
87 params = []
88 for name in ("host", "port", "dbname", "user", "password"):
89 val = getattr(self, name)
90 if val:
91 params.append("%s=%s" % (name, val))
92 try:
93 self.connection = psycopg.connect(" ".join(params))
94 except psycopg.OperationalError, val:
95 raise ConnectionError(str(val))
96
97 # determine the OID for the geometry type. This is PostGIS
98 # specific.
99 cursor = self.connection.cursor()
100 cursor.execute("SELECT OID, typname FROM pg_type WHERE"
101 +" typname = 'geometry'")
102 row = cursor.fetchone()
103 self.connection.commit()
104 if row is not None:
105 self.geometry_type = row[0]
106 else:
107 raise ValueError("Can't determine postgres type of geometries")
108
109 def BriefDescription(self):
110 """Return a brief, one-line description of the connection
111
112 The return value is suitable for a list box of all database
113 connections.
114 """
115 return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
116 % self.__dict__)
117
118 def Close(self):
119 """Close the database connection"""
120 self.connection.close()
121
122 def GeometryTables(self):
123 """Return a list with the names of all tables with a geometry column"""
124 cursor = self.connection.cursor()
125 cursor.execute("SELECT f_table_name FROM geometry_columns;")
126 result = [row[0] for row in cursor.fetchall()]
127 self.connection.commit()
128 return result
129
130 def cursor(self):
131 """Return a DB API 2.0 cursor for the database"""
132 return self.connection.cursor()
133
134
135
136 class PostGISColumn:
137
138 """Column description for a PostGISTable
139
140 In addition to the normal column object attributes name, type and
141 index, PostGISColumn objects have a quoted_name attribute which
142 contains a quoted version of name for use in SQL statements. The
143 quoted_name attribute is mainly intended for internal use by the
144 PostGISTable class.
145 """
146
147 def __init__(self, name, type, index):
148 self.name = name
149 self.quoted_name = quote_identifier(name)
150 self.type = type
151 self.index = index
152
153
154 class PostGISTable:
155
156 """A Table in a PostGIS database
157
158 A PostgreSQL table may contain columns with types not (yet)
159 supported by Thuban. Instances of this class ignore those columns
160 and pretend they don't exist, i.e. they won't show up in the column
161 descriptions returned by Columns() and other methods.
162 """
163
164 def __init__(self, db, tablename):
165 """Initialize the PostGISTable.
166
167 The db parameter should be an instance of PostGISConnection and
168 tablename the name of a table in the database represented by db.
169 """
170 self.db = db
171 self.tablename = tablename
172 # Tablename quoted for use in SQL statements.
173 self.quoted_tablename = quote_identifier(tablename)
174
175 # Map column names and indices to column objects.
176 self.column_map = {}
177
178 self._fetch_table_information()
179
180 def _fetch_table_information(self):
181 """Internal: Update information about the table"""
182 self.columns = []
183 cursor = self.db.cursor()
184 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename)
185 description = cursor.description
186
187 for i in range(len(description)):
188 for pgtyp, tabletyp in type_map:
189 if pgtyp == description[i][1]:
190 col = PostGISColumn(description[i][0], tabletyp,
191 len(self.columns))
192 break
193 else:
194 if description[i][1] == self.db.geometry_type:
195 self.geometry_column = description[i][0]
196 self.quoted_geo_col =quote_identifier(self.geometry_column)
197 # No matching table type. Ignore the column.
198 # FIXME: We should at least print a warning about
199 # ignored columns
200 continue
201 self.columns.append(col)
202
203 for col in self.columns:
204 self.column_map[col.name] = col
205 self.column_map[col.index] = col
206
207 # Build query string for ReadRowAsDict
208 self.query_stmt = ("SELECT %s from %s"
209 % (", ".join([col.quoted_name
210 for col in self.columns]),
211 self.quoted_tablename))
212
213 def DBConnection(self):
214 """Return the dbconnection used by the table"""
215 return self.db
216
217 def TableName(self):
218 """Return the name of the table in the database"""
219 return self.tablename
220
221 def Title(self):
222 """Return the title of the table.
223
224 The title is currently fixed and equal to the tablename
225 """
226 return self.tablename
227
228 def Dependencies(self):
229 """Return an empty tuple because a PostGISTable depends on nothing else
230 """
231 return ()
232
233 def NumColumns(self):
234 return len(self.columns)
235
236 def Columns(self):
237 return self.columns
238
239 def Column(self, col):
240 return self.column_map[col]
241
242 def HasColumn(self, col):
243 return self.column_map.has_key(col)
244
245 def NumRows(self):
246 cursor = self.db.cursor()
247 cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename)
248 return cursor.fetchone()[0]
249
250 def RowIdToOrdinal(self, gid):
251 """Return the row ordinal given its id"""
252 cursor = self.db.cursor()
253 cursor.execute("SELECT count(*) FROM %s WHERE gid < %d;"
254 % (self.quoted_tablename, gid))
255 return cursor.fetchone()[0]
256
257 def RowOrdinalToId(self, num):
258 """Return the rowid for given its ordinal"""
259 cursor = self.db.cursor()
260 cursor.execute("SELECT gid FROM %s LIMIT 1 OFFSET %d;"
261 % (self.quoted_tablename, num))
262 return cursor.fetchone()[0]
263
264 def ReadRowAsDict(self, row, row_is_ordinal = 0):
265 cursor = self.db.cursor()
266 if row_is_ordinal:
267 stmt = self.query_stmt + " LIMIT 1 OFFSET %d" % row
268 else:
269 stmt = self.query_stmt + " WHERE gid = %d" % row
270 cursor.execute(stmt)
271 result = {}
272 for col, value in zip(self.columns, cursor.fetchone()):
273 result[col.name] = value
274 return result
275
276 def ReadValue(self, row, col, row_is_ordinal = 0):
277 cursor = self.db.cursor()
278 if row_is_ordinal:
279 stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
280 (self.column_map[col].quoted_name, self.quoted_tablename,
281 row))
282 else:
283 stmt = ("SELECT %s FROM %s WHERE gid = %d" %
284 (self.column_map[col].quoted_name, self.quoted_tablename,
285 row))
286 cursor.execute(stmt)
287 return cursor.fetchone()[0]
288
289 def ValueRange(self, col):
290 cursor = self.db.cursor()
291 name = self.column_map[col].quoted_name
292 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
293 (name, name, self.quoted_tablename))
294 return tuple(cursor.fetchone())
295
296 def UniqueValues(self, col):
297 cursor = self.db.cursor()
298 name = self.column_map[col].quoted_name
299 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
300 (name, self.quoted_tablename, name))
301 return [row[0] for row in cursor.fetchall()]
302
303 def SimpleQuery(self, left, comparison, right):
304 if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
305 raise ValueError("Comparison operator %r not allowed" % comparison)
306
307 if comparison == "==":
308 comparison = "="
309
310 if isinstance(right, PostGISColumn):
311 right_template = right.quoted_name
312 params = ()
313 else:
314 right_template = "%s"
315 params = (right,)
316
317 query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
318 % (self.quoted_tablename, left.quoted_name, comparison,
319 right_template)
320
321 cursor = self.db.cursor()
322 cursor.execute(query, params)
323 result = []
324 while 1:
325 row = cursor.fetchone()
326 if row is None:
327 break
328 result.append(row[0])
329 return result
330
331
332 class PostGISShape:
333
334 def __init__(self, shapeid, data):
335 self.shapeid = shapeid
336 self.data = data
337
338 def compute_bbox(self):
339 """
340 Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
341 """
342 xs = []
343 ys = []
344 for part in self.Points():
345 for x, y in part:
346 xs.append(x)
347 ys.append(y)
348 return (min(xs), min(ys), max(xs), max(ys))
349
350 def ShapeID(self):
351 return self.shapeid
352
353 def Points(self):
354 return wellknowntext.parse_wkt_thuban(self.data)
355
356 def RawData(self):
357 return self.data
358
359
360 shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
361 "MULTIPOLYGON": SHAPETYPE_POLYGON,
362 "MULTILINESTRING": SHAPETYPE_ARC,
363 "POINT": SHAPETYPE_POINT}
364
365
366 class PostGISShapeStore(PostGISTable):
367
368 """Shapestore interface to a table in a PostGIS database"""
369
370 def Table(self):
371 """Return self since a PostGISShapeStore is its own table."""
372 return self
373
374 def OrigShapeStore(self):
375 """Return None since the PostGISShapeStore is not derived from another
376 """
377 return None
378
379 def ShapeType(self):
380 """Return the type of the shapes in the shapestore."""
381 cursor = self.db.cursor()
382 cursor.execute("SELECT type FROM geometry_columns WHERE"
383 " f_table_name=%s", (self.tablename,))
384 result = cursor.fetchone()[0]
385 cursor.close()
386 return shapetype_map[result]
387
388 def RawShapeFormat(self):
389 """Return the raw data format of the shape data.
390
391 For the PostGISShapeStore this is RAW_WKT.
392 """
393 return RAW_WKT
394
395 def NumShapes(self):
396 # The number of shapes is the same as the number of rows,
397 # assuming that the geometry can't be NULL.
398 return self.NumRows()
399
400 def BoundingBox(self):
401 """Return the bounding box of all shapes in the postgis table"""
402 minx = miny = maxx = maxy = None
403 x=[]
404 y=[]
405 cursor = self.db.cursor()
406 try:
407 # Using the extent function is postgis specific. An OGC
408 # Simple Features compliant solution would be to use a query
409 # like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and
410 # calculate the bounding box by hand from that
411 cursor.execute("SELECT extent(%s) FROM %s;"
412 % (self.quoted_geo_col, self.quoted_tablename))
413 result = cursor.fetchone()
414 if result:
415 (minx, miny), (maxx, maxy) \
416 = wellknowntext.parse_wkt_thuban(result[0])[0]
417 return (minx, miny, maxx, maxy)
418 finally:
419 cursor.close()
420
421 def Shape(self, shapeid):
422 cursor = self.db.cursor()
423 cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
424 % (self.quoted_geo_col, self.quoted_tablename, shapeid))
425 wkt = cursor.fetchone()[0]
426 cursor.close()
427 return PostGISShape(shapeid, wkt)
428
429 def AllShapes(self):
430 cursor = self.db.cursor()
431 cursor.execute("SELECT gid, AsText(%s) FROM %s ORDER BY gid"
432 % (self.quoted_geo_col, self.quoted_tablename))
433 while 1:
434 result = cursor.fetchone()
435 if result is None:
436 return
437 yield PostGISShape(result[0], result[1])
438
439
440 def ShapesInRegion(self, bbox):
441 """Generate all shapes overlapping the region given by bbox."""
442 # IMPORTANT:This will work for PostGIS < 0.8
443 left, bottom, right, top = bbox
444 geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
445 % (left, bottom, left, top, right, top, right, bottom,
446 left, bottom))
447 cursor = self.db.cursor()
448 cursor.execute("SELECT gid, AsText(%s) FROM %s"
449 " WHERE %s && GeometryFromText('%s', -1) ORDER BY gid"
450 % (self.quoted_geo_col, self.quoted_tablename,
451 self.quoted_geo_col, geom))
452 while 1:
453 result = cursor.fetchone()
454 if result is None:
455 return
456 yield PostGISShape(result[0], result[1])

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26