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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1946 - (hide 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 bh 1605 # 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 bh 1625 def has_postgis_support():
24     """Return whether this Thuban instance supports PostGIS connections
25 bh 1605
26 bh 1625 Having PostGIS support means that the psycopg module can be
27     imported.
28     """
29     return psycopg is not None
30    
31 bh 1631 def psycopg_version():
32     return psycopg.__version__
33    
34 bh 1605 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 bh 1636
40 bh 1946 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 bh 1636 class ConnectionError(Exception):
57    
58     """Class for exceptions occurring when establishing a Databse connection"""
59    
60    
61 bh 1605 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 bh 1620 self.connect()
84    
85     def connect(self):
86     """Internal: Establish the database connection"""
87 bh 1605 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 bh 1636 try:
93     self.connection = psycopg.connect(" ".join(params))
94     except psycopg.OperationalError, val:
95     raise ConnectionError(str(val))
96 bh 1605
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 bh 1620 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 bh 1605 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 bh 1946 """Column description for a PostGISTable
139 bh 1605
140 bh 1946 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 bh 1605 def __init__(self, name, type, index):
148     self.name = name
149 bh 1946 self.quoted_name = quote_identifier(name)
150 bh 1605 self.type = type
151     self.index = index
152    
153    
154     class PostGISTable:
155    
156 bh 1693 """A Table in a PostGIS database
157 bh 1605
158 bh 1693 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 bh 1605 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 bh 1946 # 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 bh 1605 self.column_map = {}
177 bh 1946
178 bh 1605 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 bh 1946 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename)
185 bh 1605 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 bh 1693 col = PostGISColumn(description[i][0], tabletyp,
191     len(self.columns))
192 bh 1605 break
193     else:
194     if description[i][1] == self.db.geometry_type:
195     self.geometry_column = description[i][0]
196 bh 1946 self.quoted_geo_col =quote_identifier(self.geometry_column)
197 bh 1605 # 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 bh 1946 % (", ".join([col.quoted_name
210     for col in self.columns]),
211     self.quoted_tablename))
212 bh 1605
213 bh 1638 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 bh 1658 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 bh 1605 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 bh 1946 cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename)
248 bh 1605 return cursor.fetchone()[0]
249    
250 bh 1662 def RowIdToOrdinal(self, gid):
251     """Return the row ordinal given its id"""
252 bh 1605 cursor = self.db.cursor()
253 bh 1662 cursor.execute("SELECT count(*) FROM %s WHERE gid < %d;"
254 bh 1946 % (self.quoted_tablename, gid))
255 bh 1662 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 bh 1946 % (self.quoted_tablename, num))
262 bh 1662 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 bh 1605 result = {}
272     for col, value in zip(self.columns, cursor.fetchone()):
273     result[col.name] = value
274     return result
275    
276 bh 1662 def ReadValue(self, row, col, row_is_ordinal = 0):
277 bh 1605 cursor = self.db.cursor()
278 bh 1662 if row_is_ordinal:
279     stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
280 bh 1946 (self.column_map[col].quoted_name, self.quoted_tablename,
281     row))
282 bh 1662 else:
283     stmt = ("SELECT %s FROM %s WHERE gid = %d" %
284 bh 1946 (self.column_map[col].quoted_name, self.quoted_tablename,
285     row))
286 bh 1662 cursor.execute(stmt)
287 bh 1605 return cursor.fetchone()[0]
288    
289     def ValueRange(self, col):
290     cursor = self.db.cursor()
291 bh 1946 name = self.column_map[col].quoted_name
292 bh 1605 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
293 bh 1946 (name, name, self.quoted_tablename))
294 bh 1605 return tuple(cursor.fetchone())
295    
296     def UniqueValues(self, col):
297     cursor = self.db.cursor()
298 bh 1946 name = self.column_map[col].quoted_name
299 bh 1605 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
300 bh 1946 (name, self.quoted_tablename, name))
301 bh 1605 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 bh 1946 right_template = right.quoted_name
312 bh 1605 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 bh 1946 % (self.quoted_tablename, left.quoted_name, comparison,
319     right_template)
320 bh 1605
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 bh 1656 "MULTIPOLYGON": SHAPETYPE_POLYGON,
362 bh 1605 "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 bh 1660 # 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 bh 1946 % (self.quoted_geo_col, self.quoted_tablename))
413 bh 1605 result = cursor.fetchone()
414 bh 1660 if result:
415     (minx, miny), (maxx, maxy) \
416     = wellknowntext.parse_wkt_thuban(result[0])[0]
417     return (minx, miny, maxx, maxy)
418 bh 1605 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 bh 1946 % (self.quoted_geo_col, self.quoted_tablename, shapeid))
425 bh 1605 wkt = cursor.fetchone()[0]
426     cursor.close()
427     return PostGISShape(shapeid, wkt)
428    
429 bh 1658 def AllShapes(self):
430     cursor = self.db.cursor()
431     cursor.execute("SELECT gid, AsText(%s) FROM %s ORDER BY gid"
432 bh 1946 % (self.quoted_geo_col, self.quoted_tablename))
433 bh 1658 while 1:
434     result = cursor.fetchone()
435     if result is None:
436     return
437     yield PostGISShape(result[0], result[1])
438    
439    
440 bh 1605 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 bh 1946 % (self.quoted_geo_col, self.quoted_tablename,
451     self.quoted_geo_col, geom))
452 bh 1605 while 1:
453     result = cursor.fetchone()
454     if result is None:
455 bh 1658 return
456 bh 1605 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