/[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 2106 - (show annotations)
Fri Mar 12 12:59:33 2004 UTC (21 years ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 25691 byte(s)
Support views in addition to normal tables in the postgis shapestore

* Thuban/Model/postgisdb.py
(PostGISShapeStore._fetch_table_information): Add a fallback for
the case where the table name is not in the geometry_columns
table.  This is usually the case for views.  Also, set
self.shapestore here.
(PostGISShapeStore.ShapeType): No need to query the database all
the time.  The shape type is now determined in
_fetch_table_information

* test/postgissupport.py (PostgreSQLServer.new_postgis_db)
(PostgreSQLServer.get_static_data_db, PostGISDatabase.__init__):
New parameter to specify views.
(PostGISDatabase.has_data): Also compare the views.  New views
parameter
(PostGISDatabase.initdb): Create the views.
(PostgreSQLServer.get_default_static_data_db): Add the v_landmarks
view

* test/test_postgis_db.py
(TestPostGISShapestorePointFromViews): New.  Test a
PostGISShapeStore with a view
(TestPostGISShapestorePointOIDAsGIDColumn.setUp): Pass the name of
the geometry_column explicitly to test whether that works

1 # Copyright (C) 2003, 2004 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 maps psycopg type objects. It's a list of pairs since
36 # the psycopg type objects are unhashable.
37 type_map = [(psycopg.STRING, table.FIELDTYPE_STRING),
38 (psycopg.INTEGER, table.FIELDTYPE_INT),
39 (psycopg.ROWID, table.FIELDTYPE_INT),
40 (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
41
42 # _raw_type_map maps the postgresql type constants to Thuban type
43 # constants. This is very low level and postgresql specific and
44 # should be used only when necessary.
45 _raw_type_map = {}
46 def _fill_raw_type_map():
47 for psycopg_type, thuban_type in type_map:
48 for value in psycopg_type.values:
49 _raw_type_map[value] = thuban_type
50 _fill_raw_type_map()
51
52
53 def quote_identifier(ident):
54 """Return a quoted version of the identifier ident.
55
56 The return value is a string that can be put directly into an SQL
57 statement. The quoted identifier is surrounded by double quotes and
58 any double quotes already in the input value are converted to two
59 double quotes. Examples:
60
61 >>> quote_identifier("abc\"def")
62 '"abc""def"'
63 >>> quote_identifier("abc def")
64 '"abc def"'
65 """
66 return '"' + '""'.join(ident.split('"')) + '"'
67
68
69 class ConnectionError(Exception):
70
71 """Class for exceptions occurring when establishing a Databse connection"""
72
73
74 class PostGISConnection:
75
76 """Represent a PostGIS database
77
78 A PostGISConnection instance has the following public attributes:
79
80 dbname -- The name of the database
81 host, port -- Host and port to connect to
82 user -- The user name to connect as.
83
84 All of these attributes are strings and may be empty strings to
85 indicate default values.
86 """
87
88 def __init__(self, dbname, host="", user="", password="", dbtype="",
89 port=""):
90 self.dbname = dbname
91 self.host = host
92 self.port = port
93 self.user = user
94 self.password = password
95 self.dbtype = dbtype
96 self.connect()
97
98 def connect(self):
99 """Internal: Establish the database connection"""
100 params = []
101 for name in ("host", "port", "dbname", "user", "password"):
102 val = getattr(self, name)
103 if val:
104 params.append("%s=%s" % (name, val))
105 try:
106 self.connection = psycopg.connect(" ".join(params))
107 except psycopg.OperationalError, val:
108 raise ConnectionError(str(val))
109
110 # determine the OID for the geometry type. This is PostGIS
111 # specific.
112 cursor = self.connection.cursor()
113 cursor.execute("SELECT OID, typname FROM pg_type WHERE"
114 +" typname = 'geometry'")
115 row = cursor.fetchone()
116 self.connection.commit()
117 if row is not None:
118 self.geometry_type = row[0]
119 else:
120 raise ValueError("Can't determine postgres type of geometries")
121
122 def BriefDescription(self):
123 """Return a brief, one-line description of the connection
124
125 The return value is suitable for a list box of all database
126 connections.
127 """
128 return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
129 % self.__dict__)
130
131 def MatchesParameters(self, parameters):
132 """Return whether the connection matches the dictionary of parameters
133
134 Return whether instatiating the connection with the given
135 parameters would establish essentially the same connection as
136 self. The connection is essentially the same if the same
137 database (identified by host, port and databasename) is accessed
138 as the same user.
139 """
140 return (parameters["host"] == self.host
141 and parameters["port"] == self.port
142 and parameters["dbname"] == self.dbname
143 and parameters["user"] == self.user)
144
145 def Close(self):
146 """Close the database connection"""
147 self.connection.close()
148
149 def GeometryTables(self):
150 """Return a list with the names of all tables with a geometry column"""
151
152 # The query is basically taken from the psql v. 7.2.1. When
153 # started with -E it prints the queries used for internal
154 # commands such as \d, which does mostly what we need here.
155 cursor = self.connection.cursor()
156 cursor.execute("SELECT c.relname FROM pg_class c"
157 " WHERE c.relkind IN ('r', 'v')"
158 # Omit the system tables
159 " AND c.relname !~ '^pg_'"
160 # Omit the special PostGIS tables
161 " AND c.relname NOT IN ('geometry_columns',"
162 " 'spatial_ref_sys')"
163 " AND %d in (SELECT a.atttypid FROM pg_attribute a"
164 " WHERE a.attrelid = c.oid)"
165 " ORDER BY c.relname;", (self.geometry_type,))
166 result = [row[0] for row in cursor.fetchall()]
167 self.connection.commit()
168 return result
169
170 def table_columns(self, tablename):
171 """Experimental: return information about the columns of a table
172
173 Return value is a list of (name, type) pairs where name is the
174 name of the column and type either one of the field type columns
175 or the string 'geometry' indicating a geometry column.
176
177 The intended use of this method is for table selection dialogs
178 which need to determine which columns are usable as id or
179 geometry columns respectively. Suitable id columns will have
180 type FIELDTYPE_INT and geometry columns will have 'geometry'.
181 """
182 result = []
183 cursor = self.connection.cursor()
184
185 # This query is taken basically from the \d command of psql
186 # 7.2.1
187 cursor.execute("SELECT a.attname, a.atttypid, a.attnum"
188 " FROM pg_class c, pg_attribute a"
189 " WHERE c.relname = %s AND a.attrelid = c.oid"
190 " ORDER BY a.attnum;", (tablename,))
191
192 for row in cursor.fetchall():
193 col_name, col_type, col_attnum = row
194 col = None
195 if col_attnum < 1:
196 # It's a system column. Only the OID is interesting
197 # here
198 if col_name == "oid":
199 col = (col_name, _raw_type_map[col_type])
200 else:
201 # If it's an integer
202 thuban_type = _raw_type_map.get(col_type)
203 if thuban_type is not None:
204 col = (col_name, thuban_type)
205 elif row[1] == self.geometry_type:
206 col = (col_name, "geometry")
207 if col is not None:
208 result.append(col)
209
210 return result
211
212 def cursor(self):
213 """Return a DB API 2.0 cursor for the database"""
214 return self.connection.cursor()
215
216
217
218 class PostGISColumn:
219
220 """Column description for a PostGISTable
221
222 In addition to the normal column object attributes name, type and
223 index, PostGISColumn objects have a quoted_name attribute which
224 contains a quoted version of name for use in SQL statements. The
225 quoted_name attribute is mainly intended for internal use by the
226 PostGISTable class.
227 """
228
229 def __init__(self, name, type, index):
230 self.name = name
231 self.quoted_name = quote_identifier(name)
232 self.type = type
233 self.index = index
234
235
236 class PostGISTable:
237
238 """A Table in a PostGIS database
239
240 A PostgreSQL table may contain columns with types not (yet)
241 supported by Thuban. Instances of this class ignore those columns
242 and pretend they don't exist, i.e. they won't show up in the column
243 descriptions returned by Columns() and other methods.
244 """
245
246 def __init__(self, db, tablename, id_column = None):
247 """Initialize the PostGISTable.
248
249 The db parameter should be an instance of PostGISConnection and
250 tablename the name of a table in the database represented by db.
251
252 The id_column parameter should be the name of a column in the
253 table that can be used to identify rows. The column must have
254 the type integer and be unique and not null.
255
256 For backwards compatibility reasons, the id_column parameter is
257 optional. If not given the table must have a column called
258 'gid' which is used as the id_column. New code should always
259 provide this parameter.
260 """
261 self.db = db
262 self.tablename = tablename
263 # Tablename quoted for use in SQL statements.
264 self.quoted_tablename = quote_identifier(tablename)
265
266 if not id_column:
267 id_column = "gid"
268 self.id_column = id_column
269 # id column name quoted for use in SQL statements.
270 self.quoted_id_column = quote_identifier(id_column)
271
272 # Map column names and indices to column objects.
273 self.column_map = {}
274
275 self._fetch_table_information()
276
277 def _fetch_table_information(self):
278 """Internal: Update information about the table"""
279 self.columns = []
280 cursor = self.db.cursor()
281 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename)
282 description = cursor.description
283
284 for i in range(len(description)):
285 col = self._create_col_from_description(i, description[i])
286 if col is not None:
287 self.columns.append(col)
288
289 for col in self.columns:
290 self.column_map[col.name] = col
291 self.column_map[col.index] = col
292
293 # Build query string for ReadRowAsDict
294 self.query_stmt = ("SELECT %s from %s"
295 % (", ".join([col.quoted_name
296 for col in self.columns]),
297 self.quoted_tablename))
298
299 def _create_col_from_description(self, index, description):
300 """Return the column object for the column described by description
301
302 The parameter index is the index of the column. The description
303 is a sequence taken from the cursor's description attribute for
304 the column. That means description[0] is the name of the column
305 and description[1] the type.
306
307 Return None if the column can't be represented for some reason,
308 e.g. because its type is not yet supported or needs to be
309 treated in some special way. Derived classes may extend this
310 method.
311 """
312 for pgtyp, tabletyp in type_map:
313 if pgtyp == description[1]:
314 return PostGISColumn(description[0], tabletyp,
315 len(self.columns))
316 return None
317
318 def DBConnection(self):
319 """Return the dbconnection used by the table"""
320 return self.db
321
322 def IDColumn(self):
323 """Return the column description object for the id column.
324
325 If the oid column was used as the id column, the return value is
326 not one of the regular column objects that would be returned by
327 e.g. the Column() method, but it still has meaningful name
328 attribute.
329 """
330 if self.id_column == "oid":
331 return PostGISColumn(self.id_column, table.FIELDTYPE_INT, None)
332 return self.column_map[self.id_column]
333
334 def TableName(self):
335 """Return the name of the table in the database"""
336 return self.tablename
337
338 def Title(self):
339 """Return the title of the table.
340
341 The title is currently fixed and equal to the tablename
342 """
343 return self.tablename
344
345 def Dependencies(self):
346 """Return an empty tuple because a PostGISTable depends on nothing else
347 """
348 return ()
349
350 def NumColumns(self):
351 return len(self.columns)
352
353 def Columns(self):
354 return self.columns
355
356 def Column(self, col):
357 return self.column_map[col]
358
359 def HasColumn(self, col):
360 return self.column_map.has_key(col)
361
362 def NumRows(self):
363 cursor = self.db.cursor()
364 cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename)
365 return cursor.fetchone()[0]
366
367 def RowIdToOrdinal(self, gid):
368 """Return the row ordinal given its id"""
369 cursor = self.db.cursor()
370 cursor.execute("SELECT count(*) FROM %s WHERE %s < %d;"
371 % (self.quoted_tablename, self.quoted_id_column, gid))
372 return cursor.fetchone()[0]
373
374 def RowOrdinalToId(self, num):
375 """Return the rowid for given its ordinal"""
376 cursor = self.db.cursor()
377 cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d;"
378 % (self.quoted_id_column, self.quoted_tablename, num))
379 return cursor.fetchone()[0]
380
381 def ReadRowAsDict(self, row, row_is_ordinal = 0):
382 cursor = self.db.cursor()
383 if row_is_ordinal:
384 stmt = self.query_stmt + " LIMIT 1 OFFSET %d" % row
385 else:
386 stmt = self.query_stmt + " WHERE %s = %d" % (self.quoted_id_column,
387 row)
388 cursor.execute(stmt)
389 result = {}
390 for col, value in zip(self.columns, cursor.fetchone()):
391 result[col.name] = value
392 return result
393
394 def ReadValue(self, row, col, row_is_ordinal = 0):
395 cursor = self.db.cursor()
396 if row_is_ordinal:
397 stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
398 (self.column_map[col].quoted_name, self.quoted_tablename,
399 row))
400 else:
401 stmt = ("SELECT %s FROM %s WHERE %s = %d" %
402 (self.column_map[col].quoted_name, self.quoted_tablename,
403 self.quoted_id_column, row))
404 cursor.execute(stmt)
405 return cursor.fetchone()[0]
406
407 def ValueRange(self, col):
408 cursor = self.db.cursor()
409 name = self.column_map[col].quoted_name
410 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
411 (name, name, self.quoted_tablename))
412 return tuple(cursor.fetchone())
413
414 def UniqueValues(self, col):
415 cursor = self.db.cursor()
416 name = self.column_map[col].quoted_name
417 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
418 (name, self.quoted_tablename, name))
419 return [row[0] for row in cursor.fetchall()]
420
421 def SimpleQuery(self, left, comparison, right):
422 if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
423 raise ValueError("Comparison operator %r not allowed" % comparison)
424
425 if comparison == "==":
426 comparison = "="
427
428 if isinstance(right, PostGISColumn):
429 right_template = right.quoted_name
430 params = ()
431 else:
432 right_template = "%s"
433 params = (right,)
434
435 query = "SELECT %s FROM %s WHERE %s %s %s ORDER BY %s;" \
436 % (self.quoted_id_column, self.quoted_tablename,
437 left.quoted_name, comparison, right_template,
438 self.quoted_id_column)
439
440 cursor = self.db.cursor()
441 cursor.execute(query, params)
442 result = []
443 while 1:
444 row = cursor.fetchone()
445 if row is None:
446 break
447 result.append(row[0])
448 return result
449
450
451 class PostGISShape:
452
453 def __init__(self, shapeid, data):
454 self.shapeid = shapeid
455 self.data = data
456
457 def compute_bbox(self):
458 """
459 Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
460 """
461 xs = []
462 ys = []
463 for part in self.Points():
464 for x, y in part:
465 xs.append(x)
466 ys.append(y)
467 return (min(xs), min(ys), max(xs), max(ys))
468
469 def ShapeID(self):
470 return self.shapeid
471
472 def Points(self):
473 return wellknowntext.parse_wkt_thuban(self.data)
474
475 def RawData(self):
476 return self.data
477
478
479 shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
480 "MULTIPOLYGON": SHAPETYPE_POLYGON,
481 "MULTILINESTRING": SHAPETYPE_ARC,
482 "POINT": SHAPETYPE_POINT}
483
484
485 class PostGISShapeStore(PostGISTable):
486
487 """Shapestore interface to a table in a PostGIS database"""
488
489 def __init__(self, db, tablename, id_column = "gid",
490 geometry_column = None):
491 """Initialize the PostGISShapeStore.
492
493 The db parameter should be an instance of PostGISConnection and
494 tablename the name of a table in the database represented by db.
495
496 The id_column parameter should be the name of a column in the
497 table that can be used to identify rows. The column must have
498 the type integer and be unique and not null.
499
500 The geometry_column paramter, if given, should be the name of
501 the geometry column to use. If the name given is not a geometry
502 column, raise a ValueError.
503
504 If no geometry_column is given, the table must have exactly one
505 geometry column. If it has more than one and the
506 geometry_column is not given, a ValueError will be raised.
507 """
508 self.geometry_column = geometry_column
509 self.geometry_column_was_given = geometry_column is not None
510 PostGISTable.__init__(self, db, tablename, id_column)
511
512 # For convenience, we have a quoted version of the geometry
513 # column in self.quoted_geo_col
514 self.quoted_geo_col = quote_identifier(self.geometry_column)
515
516 def _fetch_table_information(self):
517 """Extend inherited method to retrieve the SRID and shape type"""
518 PostGISTable._fetch_table_information(self)
519
520 # First, try to get it from the geometry_columns table.
521 cursor = self.db.cursor()
522 cursor.execute("SELECT srid, type FROM geometry_columns"
523 " WHERE f_table_name = %s AND f_geometry_column=%s",
524 (self.tablename, self.geometry_column))
525 row = cursor.fetchone()
526 if row is not None:
527 self.srid = row[0]
528 self.shape_type = shapetype_map.get(row[1])
529 return
530
531 # The table is probably really a view and thus not in
532 # geometry_columns. Use a different approach
533 cursor = self.db.cursor()
534 cursor.execute("SELECT DISTINCT SRID(%s) FROM %s;" %
535 (quote_identifier(self.geometry_column),
536 self.tablename))
537 row = cursor.fetchone()
538 if row is not None:
539 self.srid = row[0]
540 # Try to see whether there's another one
541 row = cursor.fetchone()
542 if row is not None:
543 # There are at least two different srids. We don't
544 # support that
545 self.srid = None
546
547 cursor = self.db.cursor()
548 cursor.execute("SELECT DISTINCT GeometryType(%s) FROM %s;"
549 % (quote_identifier(self.geometry_column),
550 self.tablename))
551 row = cursor.fetchone()
552 if row is not None:
553 self.shape_type = shapetype_map.get(row[0])
554 # Try to see whether there's another one
555 row = cursor.fetchone()
556 if row is not None:
557 # There are at least two different srids. We don't
558 # support that
559 self.shape_type = None
560
561 def _create_col_from_description(self, index, description):
562 """Extend the inherited method to find geometry columns
563
564 If the column indicated by the parameters is a geometry column,
565 record its name in self.geometry_column and a quoted version in
566 self.quoted_geo_col. In any case return the return value of the
567 inherited method.
568 """
569 col = PostGISTable._create_col_from_description(self, index,
570 description)
571 col_name, col_type = description[:2]
572 if self.geometry_column_was_given:
573 if (col_name == self.geometry_column
574 and col_type != self.db.geometry_type):
575 raise TypeError("Column %s in %s is not a geometry column"
576 % (self.geometry_column, self.tablename))
577 else:
578 if col is None:
579 if description[1] == self.db.geometry_type:
580 # The column is a geometry column. If the name of
581 # the geometry column was not given to the
582 # constructor, and we encounter two geometry
583 # columns, raise a value error
584 if self.geometry_column is None:
585 self.geometry_column = description[0]
586 else:
587 raise TypeError("Table %s has two geometry columns"
588 " and no column name was given"
589 % (self.tablename,))
590 return col
591
592 def Table(self):
593 """Return self since a PostGISShapeStore is its own table."""
594 return self
595
596 def OrigShapeStore(self):
597 """Return None since the PostGISShapeStore is not derived from another
598 """
599 return None
600
601 def GeometryColumn(self):
602 """Return the column description object for the geometry column
603
604 There's currently no FIELDTYPE constant for this column, so the
605 return value is not a regular column object that could also be
606 returned from e.g. the Column() method. Only the name attribute
607 of the return value is meaningful at the moment.
608 """
609 return PostGISColumn(self.geometry_column, None, None)
610
611 def ShapeType(self):
612 """Return the type of the shapes in the shapestore."""
613 return self.shape_type
614
615 def RawShapeFormat(self):
616 """Return the raw data format of the shape data.
617
618 For the PostGISShapeStore this is RAW_WKT.
619 """
620 return RAW_WKT
621
622 def NumShapes(self):
623 # The number of shapes is the same as the number of rows,
624 # assuming that the geometry can't be NULL.
625 return self.NumRows()
626
627 def BoundingBox(self):
628 """Return the bounding box of all shapes in the postgis table"""
629 minx = miny = maxx = maxy = None
630 x=[]
631 y=[]
632 cursor = self.db.cursor()
633 try:
634 # Using the extent function is postgis specific. An OGC
635 # Simple Features compliant solution would be to use a query
636 # like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and
637 # calculate the bounding box by hand from that
638 cursor.execute("SELECT extent(%s) FROM %s;"
639 % (self.quoted_geo_col, self.quoted_tablename))
640 result = cursor.fetchone()
641 if result[0]:
642 (minx, miny), (maxx, maxy) \
643 = wellknowntext.parse_wkt_thuban(result[0])[0]
644 return (minx, miny, maxx, maxy)
645 finally:
646 cursor.close()
647
648 def Shape(self, shapeid):
649 cursor = self.db.cursor()
650 cursor.execute("SELECT AsText(%s) FROM %s WHERE %s=%d"
651 % (self.quoted_geo_col, self.quoted_tablename,
652 self.quoted_id_column, shapeid))
653 wkt = cursor.fetchone()[0]
654 cursor.close()
655 return PostGISShape(shapeid, wkt)
656
657 def AllShapes(self):
658 cursor = self.db.cursor()
659 cursor.execute("SELECT %s, AsText(%s) FROM %s ORDER BY %s"
660 % (self.quoted_id_column, self.quoted_geo_col,
661 self.quoted_tablename, self.quoted_id_column))
662 while 1:
663 result = cursor.fetchone()
664 if result is None:
665 return
666 yield PostGISShape(result[0], result[1])
667
668
669 def ShapesInRegion(self, bbox):
670 """Generate all shapes overlapping the region given by bbox."""
671 # IMPORTANT:This will work for PostGIS < 0.8
672 left, bottom, right, top = bbox
673 geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
674 % (left, bottom, left, top, right, top, right, bottom,
675 left, bottom))
676 cursor = self.db.cursor()
677 cursor.execute("SELECT %(gid)s, AsText(%(geom)s) FROM %(table)s"
678 " WHERE %(geom)s && GeometryFromText('%(box)s', %(srid)d)"
679 " ORDER BY %(gid)s"
680 % {"table": self.quoted_tablename,
681 "geom": self.quoted_geo_col,
682 "gid": self.quoted_id_column,
683 "box": geom,
684 "srid": self.srid})
685 while 1:
686 result = cursor.fetchone()
687 if result is None:
688 return
689 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