core.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322
  1. """
  2. This module provides the functionality to create the temporal
  3. SQL database and to establish a connection to the database.
  4. Usage:
  5. .. code-block:: python
  6. >>> import grass.temporal as tgis
  7. >>> # Create the temporal database
  8. >>> tgis.init()
  9. >>> # Establish a database connection
  10. >>> dbif, connected = tgis.init_dbif(None)
  11. >>> dbif.connect()
  12. >>> # Execute a SQL statement
  13. >>> dbif.execute_transaction("SELECT datetime(0, 'unixepoch', 'localtime');")
  14. >>> # Mogrify an SQL statement
  15. >>> dbif.mogrify_sql_statement(["SELECT name from raster_base where name = ?",
  16. ... ("precipitation",)])
  17. "SELECT name from raster_base where name = 'precipitation'"
  18. >>> dbif.close()
  19. (C) 2011-2014 by the GRASS Development Team
  20. This program is free software under the GNU General Public
  21. License (>=v2). Read the file COPYING that comes with GRASS
  22. for details.
  23. :author: Soeren Gebbert
  24. """
  25. import os
  26. # i18N
  27. import gettext
  28. gettext.install('grasslibs', os.path.join(os.getenv("GISBASE"), 'locale'))
  29. import grass.script as gscript
  30. from datetime import datetime
  31. from c_libraries_interface import *
  32. # Import all supported database backends
  33. # Ignore import errors since they are checked later
  34. try:
  35. import sqlite3
  36. except ImportError:
  37. pass
  38. # Postgresql is optional, existence is checked when needed
  39. try:
  40. import psycopg2
  41. import psycopg2.extras
  42. except:
  43. pass
  44. import atexit
  45. ###############################################################################
  46. def profile_function(func):
  47. """Profiling function provided by the temporal framework"""
  48. do_profiling = os.getenv("GRASS_TGIS_PROFILE")
  49. if do_profiling is "True" or do_profiling is "1":
  50. import cProfile, pstats, StringIO
  51. pr = cProfile.Profile()
  52. pr.enable()
  53. func()
  54. pr.disable()
  55. s = StringIO.StringIO()
  56. sortby = 'cumulative'
  57. ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
  58. ps.print_stats()
  59. print s.getvalue()
  60. else:
  61. func()
  62. # Global variable that defines the backend
  63. # of the temporal GIS
  64. # It can either be "sqlite" or "pg"
  65. tgis_backend = None
  66. def get_tgis_backend():
  67. """Return the temporal GIS backend as string
  68. :returns: either "sqlite" or "pg"
  69. """
  70. global tgis_backend
  71. return tgis_backend
  72. # Global variable that defines the database string
  73. # of the temporal GIS
  74. tgis_database = None
  75. def get_tgis_database():
  76. """Return the temporal database string specified with t.connect
  77. """
  78. global tgis_database
  79. return tgis_database
  80. # The version of the temporal framework
  81. # this value must be an integer larger than 0
  82. # Increase this value in case of backward incompatible changes in the TGIS API
  83. tgis_version = 2
  84. # The version of the temporal database since framework and database version
  85. # can differ this value must be an integer larger than 0
  86. # Increase this value in case of backward incompatible changes
  87. # temporal database SQL layout
  88. tgis_db_version = 2
  89. # We need to know the parameter style of the database backend
  90. tgis_dbmi_paramstyle = None
  91. def get_tgis_dbmi_paramstyle():
  92. """Return the temporal database backend parameter style
  93. :returns: "qmark" or ""
  94. """
  95. global tgis_dbmi_paramstyle
  96. return tgis_dbmi_paramstyle
  97. # We need to access the current mapset quite often in the framework, so we make
  98. # a global variable that will be initiated when init() is called
  99. current_mapset = None
  100. current_location = None
  101. current_gisdbase = None
  102. ###############################################################################
  103. def get_current_mapset():
  104. """Return the current mapset
  105. This is the fastest way to receive the current mapset.
  106. The current mapset is set by init() and stored in a global variable.
  107. This function provides access to this global variable.
  108. """
  109. global current_mapset
  110. return current_mapset
  111. ###############################################################################
  112. def get_current_location():
  113. """Return the current location
  114. This is the fastest way to receive the current location.
  115. The current location is set by init() and stored in a global variable.
  116. This function provides access to this global variable.
  117. """
  118. global current_location
  119. return current_location
  120. ###############################################################################
  121. def get_current_gisdbase():
  122. """Return the current gis database (gisdbase)
  123. This is the fastest way to receive the current gisdbase.
  124. The current gisdbase is set by init() and stored in a global variable.
  125. This function provides access to this global variable.
  126. """
  127. global current_gisdbase
  128. return current_gisdbase
  129. ###############################################################################
  130. # If this global variable is set True, then maps can only be registered in
  131. # space time datasets with the same mapset. In addition, only maps in the
  132. # current mapset can be inserted, updated or deleted from the temporal database.
  133. # Overwrite this global variable by: g.gisenv set="TGIS_DISABLE_MAPSET_CHECK=True"
  134. # ATTENTION: Be aware to face corrupted temporal database in case this global
  135. # variable is set to False. This feature is highly
  136. # experimental and violates the grass permission guidance.
  137. enable_mapset_check = True
  138. # If this global variable is set True, the timestamps of maps will be written
  139. # as textfiles for each map that will be inserted or updated in the temporal
  140. # database using the C-library timestamp interface.
  141. # Overwrite this global variable by: g.gisenv set="TGIS_DISABLE_TIMESTAMP_WRITE=True"
  142. # ATTENTION: Be aware to face corrupted temporal database in case this global
  143. # variable is set to False. This feature is highly
  144. # experimental and violates the grass permission guidance.
  145. enable_timestamp_write = True
  146. def get_enable_mapset_check():
  147. """Return True if the mapsets should be checked while insert, update,
  148. delete requests and space time dataset registration.
  149. If this global variable is set True, then maps can only be registered
  150. in space time datasets with the same mapset. In addition, only maps in
  151. the current mapset can be inserted, updated or deleted from the temporal
  152. database.
  153. Overwrite this global variable by: g.gisenv set="TGIS_DISABLE_MAPSET_CHECK=True"
  154. ..warning::
  155. Be aware to face corrupted temporal database in case this
  156. global variable is set to False. This feature is highly
  157. experimental and violates the grass permission guidance.
  158. """
  159. global enable_mapset_check
  160. return enable_mapset_check
  161. def get_enable_timestamp_write():
  162. """Return True if the map timestamps should be written to the spatial
  163. database metadata as well.
  164. If this global variable is set True, the timestamps of maps will be
  165. written as textfiles for each map that will be inserted or updated in
  166. the temporal database using the C-library timestamp interface.
  167. Overwrite this global variable by: g.gisenv set="TGIS_DISABLE_TIMESTAMP_WRITE=True"
  168. ..warning::
  169. Be aware that C-libraries can not access timestamp informations if
  170. they are not written as spatial database metadata, hence modules
  171. that make use of timestamps using the C-library interface will not
  172. work with maps that were created without writing the timestamps.
  173. """
  174. global enable_timestamp_write
  175. return enable_timestamp_write
  176. ###############################################################################
  177. # The global variable that stores the PyGRASS Messenger object that
  178. # provides a fast and exit safe interface to the C-library message functions
  179. message_interface = None
  180. def _init_tgis_message_interface(raise_on_error=False):
  181. """Initiate the global mesage interface
  182. :param raise_on_error: If True raise a FatalError exception in case of
  183. a fatal error, call sys.exit(1) otherwise
  184. """
  185. global message_interface
  186. from grass.pygrass import messages
  187. message_interface = messages.get_msgr(raise_on_error=raise_on_error)
  188. def get_tgis_message_interface():
  189. """Return the temporal GIS message interface which is of type
  190. grass.pyhrass.message.Messenger()
  191. Use this message interface to print messages to stdout using the
  192. GRASS C-library messaging system.
  193. """
  194. global message_interface
  195. return message_interface
  196. ###############################################################################
  197. # The global variable that stores the C-library interface object that
  198. # provides a fast and exit safe interface to the C-library libgis,
  199. # libraster, libraster3d and libvector functions
  200. c_library_interface = None
  201. def _init_tgis_c_library_interface():
  202. """Set the global C-library interface variable that
  203. provides a fast and exit safe interface to the C-library libgis,
  204. libraster, libraster3d and libvector functions
  205. """
  206. global c_library_interface
  207. c_library_interface = CLibrariesInterface()
  208. def get_tgis_c_library_interface():
  209. """Return the C-library interface that
  210. provides a fast and exit safe interface to the C-library libgis,
  211. libraster, libraster3d and libvector functions
  212. """
  213. global c_library_interface
  214. return c_library_interface
  215. ###############################################################################
  216. # Set this variable True to raise a FatalError exception
  217. # in case a fatal error occurs using the messenger interface
  218. raise_on_error = False
  219. def set_raise_on_error(raise_exp=True):
  220. """Define behavior on fatal error, invoked using the tgis messenger
  221. interface (msgr.fatal())
  222. The messenger interface will be restarted using the new error policy
  223. :param raise_exp: True to raise a FatalError exception instead of calling
  224. sys.exit(1) when using the tgis messenger interface
  225. .. code-block:: python
  226. >>> import grass.temporal as tgis
  227. >>> tgis.init()
  228. >>> ignore = tgis.set_raise_on_error(False)
  229. >>> msgr = tgis.get_tgis_message_interface()
  230. >>> tgis.get_raise_on_error()
  231. False
  232. >>> msgr.fatal("Ohh no no no!")
  233. Traceback (most recent call last):
  234. File "__init__.py", line 239, in fatal
  235. sys.exit(1)
  236. SystemExit: 1
  237. >>> tgis.set_raise_on_error(True)
  238. False
  239. >>> msgr.fatal("Ohh no no no!")
  240. Traceback (most recent call last):
  241. File "__init__.py", line 241, in fatal
  242. raise FatalError(message)
  243. FatalError: Ohh no no no!
  244. :returns: current status
  245. """
  246. global raise_on_error
  247. tmp_raise = raise_on_error
  248. raise_on_error = raise_exp
  249. global message_interface
  250. if message_interface:
  251. message_interface.set_raise_on_error(raise_on_error)
  252. else:
  253. _init_tgis_message_interface(raise_on_error)
  254. return tmp_raise
  255. def get_raise_on_error():
  256. """Return True if a FatalError exception is raised instead of calling
  257. sys.exit(1) in case a fatal error was invoked with msgr.fatal()
  258. """
  259. global raise_on_error
  260. return raise_on_error
  261. ###############################################################################
  262. def get_tgis_version():
  263. """Get the version number of the temporal framework
  264. :returns: The version number of the temporal framework as string
  265. """
  266. global tgis_version
  267. return tgis_version
  268. ###############################################################################
  269. def get_tgis_db_version():
  270. """Get the version number of the temporal framework
  271. :returns: The version number of the temporal framework as string
  272. """
  273. global tgis_db_version
  274. return tgis_db_version
  275. ###############################################################################
  276. def get_tgis_metadata(dbif=None):
  277. """Return the tgis metadata table as a list of rows (dicts) or None if not
  278. present
  279. :param dbif: The database interface to be used
  280. :returns: The selected rows with key/value columns or None
  281. """
  282. dbif, connected = init_dbif(dbif)
  283. # Select metadata if the table is present
  284. try:
  285. statement = "SELECT * FROM tgis_metadata;\n"
  286. dbif.execute(statement)
  287. rows = dbif.fetchall()
  288. except:
  289. rows = None
  290. if connected:
  291. dbif.close()
  292. return rows
  293. ###############################################################################
  294. # The temporal database string set with t.connect
  295. # with substituted GRASS variables gisdbase, location and mapset
  296. tgis_database_string = None
  297. def get_tgis_database_string():
  298. """Return the preprocessed temporal database string
  299. This string is the temporal database string set with t.connect
  300. that was processed to substitue location, gisdbase and mapset
  301. varibales.
  302. """
  303. global tgis_database_string
  304. return tgis_database_string
  305. ###############################################################################
  306. def get_sql_template_path():
  307. base = os.getenv("GISBASE")
  308. base_etc = os.path.join(base, "etc")
  309. return os.path.join(base_etc, "sql")
  310. ###############################################################################
  311. def stop_subprocesses():
  312. """Stop the messenger and C-interface subprocesses
  313. that are started by tgis.init()
  314. """
  315. global message_interface
  316. global c_library_interface
  317. if message_interface:
  318. message_interface.stop()
  319. if c_library_interface:
  320. c_library_interface.stop()
  321. # We register this function to be called at exit
  322. atexit.register(stop_subprocesses)
  323. def get_available_temporal_mapsets():
  324. """Return a list of of mapset names with temporal database driver and names
  325. that are accessable from the current mapset.
  326. :returns: A dictionary, mapset names are keys, the tuple (driver,
  327. database) are the values
  328. """
  329. global c_library_interface
  330. mapsets = c_library_interface.available_mapsets()
  331. tgis_mapsets = {}
  332. for mapset in mapsets:
  333. driver = c_library_interface.get_driver_name(mapset)
  334. database = c_library_interface.get_database_name(mapset)
  335. if driver and database:
  336. tgis_mapsets[mapset] = (driver, database)
  337. return tgis_mapsets
  338. ###############################################################################
  339. def init(raise_fatal_error=False):
  340. """This function set the correct database backend from GRASS environmental
  341. variables and creates the grass temporal database structure for raster,
  342. vector and raster3d maps as well as for the space-time datasets strds,
  343. str3ds and stvds in case it does not exists.
  344. Several global variables are initiated and the messenger and C-library
  345. interface subprocesses are spawned.
  346. Re-run this function in case the following GRASS variables change while
  347. the process runs:
  348. - MAPSET
  349. - LOCATION_NAME
  350. - GISDBASE
  351. - TGIS_DISABLE_MAPSET_CHECK
  352. - TGIS_DISABLE_TIMESTAMP_WRITE
  353. Re-run this function if the following t.connect variables change while
  354. the process runs:
  355. - temporal GIS driver (set by t.connect driver=)
  356. - temporal GIS database (set by t.connect database=)
  357. The following environmental variables are checked:
  358. - GRASS_TGIS_PROFILE (True, False, 1, 0)
  359. - GRASS_TGIS_RAISE_ON_ERROR (True, False, 1, 0)
  360. ..warning::
  361. This functions must be called before any spatio-temporal processing
  362. can be started
  363. :param raise_fatal_error: Set this True to assure that the init()
  364. function does not kill a persistent process
  365. like the GUI. If set True a
  366. grass.pygrass.messages.FatalError
  367. exception will be raised in case a fatal
  368. error occurs in the init process, otherwise
  369. sys.exit(1) will be called.
  370. """
  371. # We need to set the correct database backend and several global variables
  372. # from the GRASS mapset specific environment variables of g.gisenv and t.connect
  373. global tgis_backend
  374. global tgis_database
  375. global tgis_database_string
  376. global tgis_dbmi_paramstyle
  377. global raise_on_error
  378. global enable_mapset_check
  379. global enable_timestamp_write
  380. global current_mapset
  381. global current_location
  382. global current_gisdbase
  383. raise_on_error = raise_fatal_error
  384. # We must run t.connect at first to create the temporal database and to
  385. # get the environmental variables
  386. gscript.run_command("t.connect", flags="c")
  387. grassenv = gscript.gisenv()
  388. # Set the global variable for faster access
  389. current_mapset = grassenv["MAPSET"]
  390. current_location = grassenv["LOCATION_NAME"]
  391. current_gisdbase = grassenv["GISDBASE"]
  392. # Check environment variable GRASS_TGIS_RAISE_ON_ERROR
  393. if os.getenv("GRASS_TGIS_RAISE_ON_ERROR") == "True" or \
  394. os.getenv("GRASS_TGIS_RAISE_ON_ERROR") == "1":
  395. raise_on_error = True
  396. # Check if the script library raises on error,
  397. # if so we do the same
  398. if gscript.get_raise_on_error() is True:
  399. raise_on_error = True
  400. # Start the GRASS message interface server
  401. _init_tgis_message_interface(raise_on_error)
  402. # Start the C-library interface server
  403. _init_tgis_c_library_interface()
  404. msgr = get_tgis_message_interface()
  405. msgr.debug(1, "Initiate the temporal database")
  406. ciface = get_tgis_c_library_interface()
  407. driver_string = ciface.get_driver_name()
  408. database_string = ciface.get_database_name()
  409. # Set the mapset check and the timestamp write
  410. if grassenv.has_key("TGIS_DISABLE_MAPSET_CHECK"):
  411. if grassenv["TGIS_DISABLE_MAPSET_CHECK"] == "True" or \
  412. grassenv["TGIS_DISABLE_MAPSET_CHECK"] == "1":
  413. enable_mapset_check = False
  414. msgr.warning("TGIS_DISABLE_MAPSET_CHECK is True")
  415. if grassenv.has_key("TGIS_DISABLE_TIMESTAMP_WRITE"):
  416. if grassenv["TGIS_DISABLE_TIMESTAMP_WRITE"] == "True" or \
  417. grassenv["TGIS_DISABLE_TIMESTAMP_WRITE"] == "1":
  418. enable_timestamp_write = False
  419. msgr.warning("TGIS_DISABLE_TIMESTAMP_WRITE is True")
  420. if driver_string is not None and driver_string is not "":
  421. if driver_string == "sqlite":
  422. tgis_backend = driver_string
  423. try:
  424. import sqlite3
  425. except ImportError:
  426. msgr.error("Unable to locate the sqlite SQL Python interface"
  427. " module sqlite3.")
  428. raise
  429. dbmi = sqlite3
  430. elif driver_string == "pg":
  431. tgis_backend = driver_string
  432. try:
  433. import psycopg2
  434. except ImportError:
  435. msgr.error("Unable to locate the Postgresql SQL Python "
  436. "interface module psycopg2.")
  437. raise
  438. dbmi = psycopg2
  439. else:
  440. msgr.fatal(_("Unable to initialize the temporal DBMI interface. "
  441. "Please use t.connect to specify the driver and the"
  442. " database string"))
  443. else:
  444. # Set the default sqlite3 connection in case nothing was defined
  445. gscript.run_command("t.connect", flags="d")
  446. driver_string = ciface.get_driver_name()
  447. database_string = ciface.get_database_name()
  448. tgis_backend = driver_string
  449. dbmi = sqlite3
  450. tgis_database_string = database_string
  451. # Set the parameter style
  452. tgis_dbmi_paramstyle = dbmi.paramstyle
  453. # We do not know if the database already exists
  454. db_exists = False
  455. dbif = SQLDatabaseInterfaceConnection()
  456. # Check if the database already exists
  457. if tgis_backend == "sqlite":
  458. # Check path of the sqlite database
  459. if os.path.exists(tgis_database_string):
  460. dbif.connect()
  461. # Check for raster_base table
  462. dbif.execute("SELECT name FROM sqlite_master WHERE type='table' "
  463. "AND name='raster_base';")
  464. name = dbif.fetchone()
  465. if name and name[0] == "raster_base":
  466. db_exists = True
  467. dbif.close()
  468. elif tgis_backend == "pg":
  469. # Connect to database
  470. dbif.connect()
  471. # Check for raster_base table
  472. dbif.execute("SELECT EXISTS(SELECT * FROM information_schema.tables "
  473. "WHERE table_name=%s)", ('raster_base',))
  474. if dbif.fetchone()[0]:
  475. db_exists = True
  476. backup_howto = "The format of your actual temporal database is not " \
  477. "supported any more.\nSolution: You need to export it by " \
  478. "restoring the GRASS GIS version used for creating this DB"\
  479. ". From there, create a backup of your temporal database "\
  480. "to avoid the loss of your temporal data.\nNotes: Use " \
  481. "t.rast.export and t.vect.export to make a backup of your" \
  482. " existing space time datasets.To safe the timestamps of" \
  483. " your existing maps and space time datasets, use " \
  484. "t.rast.list, t.vect.list and t.rast3d.list. "\
  485. "You can register the existing time stamped maps easily if"\
  486. " you export columns=id,start_time,end_time into text "\
  487. "files and use t.register to register them again in new" \
  488. " created space time datasets (t.create). After the backup"\
  489. " remove the existing temporal database, a new one will be"\
  490. " created automatically.\n"
  491. if db_exists is True:
  492. # Check the version of the temporal database
  493. dbif.close()
  494. dbif.connect()
  495. metadata = get_tgis_metadata(dbif)
  496. dbif.close()
  497. if metadata is None:
  498. msgr.fatal(_("Unable to receive temporal database metadata.\n"
  499. "Current temporal database info:%(info)s") % (
  500. {"info": get_database_info_string()}))
  501. for entry in metadata:
  502. if "tgis_version" in entry and entry[1] != str(get_tgis_version()):
  503. msgr.fatal(_("Unsupported temporal database: version mismatch."
  504. "\n %(backup)s Supported temporal API version is:"
  505. " %(api)i.\nPlease update your GRASS GIS "
  506. "installation.\nCurrent temporal database info:"
  507. "%(info)s") % ({"backup": backup_howto,
  508. "api": get_tgis_version(),
  509. "info": get_database_info_string()}))
  510. if "tgis_db_version" in entry and entry[1] != str(get_tgis_db_version()):
  511. msgr.fatal(_("Unsupported temporal database: version mismatch."
  512. "\n %(backup)sSupported temporal database version"
  513. " is: %(tdb)i\nCurrent temporal database info:"
  514. "%(info)s") % ({"backup": backup_howto,
  515. "tdb": get_tgis_version(),
  516. "info": get_database_info_string()}))
  517. return
  518. create_temporal_database(dbif)
  519. ###############################################################################
  520. def get_database_info_string():
  521. dbif = SQLDatabaseInterfaceConnection()
  522. info = "\nDBMI interface:..... " + str(dbif.get_dbmi().__name__)
  523. info += "\nTemporal database:.. " + str(get_tgis_database_string())
  524. return info
  525. ###############################################################################
  526. def create_temporal_database(dbif):
  527. """This function will create the temporal database
  528. It will create all tables and triggers that are needed to run
  529. the temporal GIS
  530. :param dbif: The database interface to be used
  531. """
  532. global tgis_backend
  533. global tgis_version
  534. global tgis_db_version
  535. global tgis_database_string
  536. template_path = get_sql_template_path()
  537. msgr = get_tgis_message_interface()
  538. # Read all SQL scripts and templates
  539. map_tables_template_sql = open(os.path.join(
  540. template_path, "map_tables_template.sql"), 'r').read()
  541. raster_metadata_sql = open(os.path.join(
  542. get_sql_template_path(), "raster_metadata_table.sql"), 'r').read()
  543. raster3d_metadata_sql = open(os.path.join(template_path,
  544. "raster3d_metadata_table.sql"),
  545. 'r').read()
  546. vector_metadata_sql = open(os.path.join(template_path,
  547. "vector_metadata_table.sql"),
  548. 'r').read()
  549. raster_views_sql = open(os.path.join(template_path, "raster_views.sql"),
  550. 'r').read()
  551. raster3d_views_sql = open(os.path.join(template_path,
  552. "raster3d_views.sql"), 'r').read()
  553. vector_views_sql = open(os.path.join(template_path, "vector_views.sql"),
  554. 'r').read()
  555. stds_tables_template_sql = open(os.path.join(template_path,
  556. "stds_tables_template.sql"),
  557. 'r').read()
  558. strds_metadata_sql = open(os.path.join(template_path,
  559. "strds_metadata_table.sql"),
  560. 'r').read()
  561. str3ds_metadata_sql = open(os.path.join(template_path,
  562. "str3ds_metadata_table.sql"),
  563. 'r').read()
  564. stvds_metadata_sql = open(os.path.join(template_path,
  565. "stvds_metadata_table.sql"),
  566. 'r').read()
  567. strds_views_sql = open(os.path.join(template_path, "strds_views.sql"),
  568. 'r').read()
  569. str3ds_views_sql = open(os.path.join(template_path, "str3ds_views.sql"),
  570. 'r').read()
  571. stvds_views_sql = open(os.path.join(template_path, "stvds_views.sql"),
  572. 'r').read()
  573. # Create the raster, raster3d and vector tables SQL statements
  574. raster_tables_sql = map_tables_template_sql.replace("GRASS_MAP", "raster")
  575. vector_tables_sql = map_tables_template_sql.replace("GRASS_MAP", "vector")
  576. raster3d_tables_sql = map_tables_template_sql.replace(
  577. "GRASS_MAP", "raster3d")
  578. # Create the space-time raster, raster3d and vector dataset tables
  579. # SQL statements
  580. strds_tables_sql = stds_tables_template_sql.replace("STDS", "strds")
  581. stvds_tables_sql = stds_tables_template_sql.replace("STDS", "stvds")
  582. str3ds_tables_sql = stds_tables_template_sql.replace("STDS", "str3ds")
  583. msgr.message(_("Creating temporal database: %s" % (tgis_database_string)))
  584. if tgis_backend == "sqlite":
  585. # We need to create the sqlite3 database path if it does not exists
  586. tgis_dir = os.path.dirname(tgis_database_string)
  587. if not os.path.exists(tgis_dir):
  588. try:
  589. os.makedirs(tgis_dir)
  590. except Exception as e:
  591. msgr.fatal(_("Unable to create SQLite temporal database\n"
  592. "Exception: %s\nPlease use t.connect to set a "
  593. "read- and writable temporal database path" % (e)))
  594. # Set up the trigger that takes care of
  595. # the correct deletion of entries across the different tables
  596. delete_trigger_sql = open(os.path.join(template_path,
  597. "sqlite3_delete_trigger.sql"),
  598. 'r').read()
  599. indexes_sql = open(os.path.join(template_path, "sqlite3_indexes.sql"),
  600. 'r').read()
  601. else:
  602. # Set up the trigger that takes care of
  603. # the correct deletion of entries across the different tables
  604. delete_trigger_sql = open(os.path.join(template_path,
  605. "postgresql_delete_trigger.sql"),
  606. 'r').read()
  607. indexes_sql = open(os.path.join(template_path,
  608. "postgresql_indexes.sql"), 'r').read()
  609. # Connect now to the database
  610. if not dbif.connected:
  611. dbif.connect()
  612. # Execute the SQL statements for sqlite
  613. # Create the global tables for the native grass datatypes
  614. dbif.execute_transaction(raster_tables_sql)
  615. dbif.execute_transaction(raster_metadata_sql)
  616. dbif.execute_transaction(raster_views_sql)
  617. dbif.execute_transaction(vector_tables_sql)
  618. dbif.execute_transaction(vector_metadata_sql)
  619. dbif.execute_transaction(vector_views_sql)
  620. dbif.execute_transaction(raster3d_tables_sql)
  621. dbif.execute_transaction(raster3d_metadata_sql)
  622. dbif.execute_transaction(raster3d_views_sql)
  623. # Create the tables for the new space-time datatypes
  624. dbif.execute_transaction(strds_tables_sql)
  625. dbif.execute_transaction(strds_metadata_sql)
  626. dbif.execute_transaction(strds_views_sql)
  627. dbif.execute_transaction(stvds_tables_sql)
  628. dbif.execute_transaction(stvds_metadata_sql)
  629. dbif.execute_transaction(stvds_views_sql)
  630. dbif.execute_transaction(str3ds_tables_sql)
  631. dbif.execute_transaction(str3ds_metadata_sql)
  632. dbif.execute_transaction(str3ds_views_sql)
  633. # The delete trigger
  634. dbif.execute_transaction(delete_trigger_sql)
  635. # The indexes
  636. dbif.execute_transaction(indexes_sql)
  637. # Create the tgis metadata table to store the database
  638. # initial configuration
  639. # The metadata table content
  640. metadata = {}
  641. metadata["tgis_version"] = tgis_version
  642. metadata["tgis_db_version"] = tgis_db_version
  643. metadata["creation_time"] = datetime.today()
  644. _create_tgis_metadata_table(metadata, dbif)
  645. dbif.close()
  646. ###############################################################################
  647. def _create_tgis_metadata_table(content, dbif=None):
  648. """!Create the temporal gis metadata table which stores all metadata
  649. information about the temporal database.
  650. :param content: The dictionary that stores the key:value metadata
  651. that should be stored in the metadata table
  652. :param dbif: The database interface to be used
  653. """
  654. dbif, connected = init_dbif(dbif)
  655. statement = "CREATE TABLE tgis_metadata (key VARCHAR NOT NULL, value VARCHAR);\n";
  656. dbif.execute_transaction(statement)
  657. for key in content.keys():
  658. statement = "INSERT INTO tgis_metadata (key, value) VALUES " + \
  659. "(\'%s\' , \'%s\');\n" % (str(key), str(content[key]))
  660. dbif.execute_transaction(statement)
  661. if connected:
  662. dbif.close()
  663. ###############################################################################
  664. class SQLDatabaseInterfaceConnection(object):
  665. def __init__(self):
  666. self.tgis_mapsets = get_available_temporal_mapsets()
  667. self.current_mapset = get_current_mapset()
  668. self.connections = {}
  669. self.connected = False
  670. self.unique_connections = {}
  671. for mapset in self.tgis_mapsets.keys():
  672. driver, dbstring = self.tgis_mapsets[mapset]
  673. if dbstring not in self.unique_connections.keys():
  674. self.unique_connections[dbstring] = DBConnection(driver)
  675. self.connections[mapset] = self.unique_connections[dbstring]
  676. self.msgr = get_tgis_message_interface()
  677. def get_dbmi(self, mapset=None):
  678. if mapset is None:
  679. mapset = self.current_mapset
  680. return self.connections[mapset].dbmi
  681. def rollback(self, mapset=None):
  682. """
  683. Roll back the last transaction. This must be called
  684. in case a new query should be performed after a db error.
  685. This is only relevant for postgresql database.
  686. """
  687. if mapset is None:
  688. mapset = self.current_mapset
  689. def connect(self):
  690. """Connect to the DBMI to execute SQL statements
  691. Supported backends are sqlite3 and postgresql
  692. """
  693. for mapset in self.tgis_mapsets.keys():
  694. driver, dbstring = self.tgis_mapsets[mapset]
  695. conn = self.connections[mapset]
  696. if conn.is_connected() is False:
  697. conn .connect(dbstring)
  698. self.connected = True
  699. def is_connected(self):
  700. return self.connected
  701. def close(self):
  702. """Close the DBMI connection
  703. There may be several temporal databases in a location, hence
  704. close all temporal databases that have been opened.
  705. """
  706. for key in self.unique_connections.keys():
  707. self.unique_connections[key] .close()
  708. self.connected = False
  709. def mogrify_sql_statement(self, content, mapset=None):
  710. """Return the SQL statement and arguments as executable SQL string
  711. :param content: The content as tuple with two entries, the first
  712. entry is the SQL statement with DBMI specific
  713. place holder (?), the second entry is the argument
  714. list that should substitute the place holder.
  715. :param mapset: The mapset of the abstract dataset or temporal
  716. database location, if None the current mapset
  717. will be used
  718. """
  719. if mapset is None:
  720. mapset = self.current_mapset
  721. if mapset not in self.tgis_mapsets.keys():
  722. self.msgr.fatal(_("Unable to mogrify sql statement. There is no "
  723. "temporal database connection defined for "
  724. "mapset <%(mapset)s>" % {"mapset": mapset}))
  725. return self.connections[mapset].mogrify_sql_statement(content)
  726. def check_table(self, table_name, mapset=None):
  727. """Check if a table exists in the temporal database
  728. :param table_name: The name of the table to be checked for existence
  729. :param mapset: The mapset of the abstract dataset or temporal
  730. database location, if None the current mapset
  731. will be used
  732. :returns: True if the table exists, False otherwise
  733. TODO:
  734. There may be several temporal databases in a location, hence
  735. the mapset is used to query the correct temporal database.
  736. """
  737. if mapset is None:
  738. mapset = self.current_mapset
  739. if mapset not in self.tgis_mapsets.keys():
  740. self.msgr.fatal(_("Unable to check table. There is no temporal "
  741. "database connection defined for mapset "
  742. "<%(mapset)s>" % {"mapset": mapset}))
  743. return self.connections[mapset].check_table(table_name)
  744. def execute(self, statement, args=None, mapset=None):
  745. """
  746. :param mapset: The mapset of the abstract dataset or temporal
  747. database location, if None the current mapset
  748. will be used
  749. """
  750. if mapset is None:
  751. mapset = self.current_mapset
  752. if mapset not in self.tgis_mapsets.keys():
  753. self.msgr.fatal(_("Unable to execute sql statement. There is no "
  754. "temporal database connection defined for "
  755. "mapset <%(mapset)s>" % {"mapset": mapset}))
  756. return self.connections[mapset].execute(statement, args)
  757. def fetchone(self, mapset=None):
  758. if mapset is None:
  759. mapset = self.current_mapset
  760. if mapset not in self.tgis_mapsets.keys():
  761. self.msgr.fatal(_("Unable to fetch one. There is no temporal "
  762. "database connection defined for mapset "
  763. "<%(mapset)s>" % {"mapset": mapset}))
  764. return self.connections[mapset].fetchone()
  765. def fetchall(self, mapset=None):
  766. if mapset is None:
  767. mapset = self.current_mapset
  768. if mapset not in self.tgis_mapsets.keys():
  769. self.msgr.fatal(_("Unable to fetch all. There is no temporal "
  770. "database connection defined for mapset "
  771. "<%(mapset)s>" % {"mapset": mapset}))
  772. return self.connections[mapset].fetchall()
  773. def execute_transaction(self, statement, mapset=None):
  774. """Execute a transactional SQL statement
  775. The BEGIN and END TRANSACTION statements will be added automatically
  776. to the sql statement
  777. :param statement: The executable SQL statement or SQL script
  778. """
  779. if mapset is None:
  780. mapset = self.current_mapset
  781. if mapset not in self.tgis_mapsets.keys():
  782. self.msgr.fatal(_("Unable to execute transaction. There is no "
  783. "temporal database connection defined for "
  784. "mapset <%(mapset)s>" % {"mapset": mapset}))
  785. return self.connections[mapset].execute_transaction(statement)
  786. ###############################################################################
  787. class DBConnection(object):
  788. """This class represents the database interface connection
  789. and provides access to the chisen backend modules.
  790. The following DBMS are supported:
  791. - sqlite via the sqlite3 standard library
  792. - postgresql via psycopg2
  793. """
  794. def __init__(self, backend=None):
  795. self.connected = False
  796. if backend is None:
  797. global tgis_backend
  798. if tgis_backend == "sqlite":
  799. self.dbmi = sqlite3
  800. else:
  801. self.dbmi = psycopg2
  802. else:
  803. if backend == "sqlite":
  804. self.dbmi = sqlite3
  805. else:
  806. self.dbmi = psycopg2
  807. self.msgr = get_tgis_message_interface()
  808. self.msgr.debug(1, "SQLDatabaseInterfaceConnection constructor")
  809. def __del__(self):
  810. if self.connected is True:
  811. self.close()
  812. def is_connected(self):
  813. return self.connected
  814. def rollback(self):
  815. """
  816. Roll back the last transaction. This must be called
  817. in case a new query should be performed after a db error.
  818. This is only relevant for postgresql database.
  819. """
  820. if self.dbmi.__name__ == "psycopg2":
  821. if self.connected:
  822. self.connection.rollback()
  823. def connect(self, dbstring=None):
  824. """Connect to the DBMI to execute SQL statements
  825. Supported backends are sqlite3 and postgresql
  826. """
  827. # Connection in the current mapset
  828. if dbstring is None:
  829. global tgis_database_string
  830. dbstring = tgis_database_string
  831. try:
  832. if self.dbmi.__name__ == "sqlite3":
  833. self.connection = self.dbmi.connect(dbstring,
  834. detect_types=self.dbmi.PARSE_DECLTYPES | self.dbmi.PARSE_COLNAMES)
  835. self.connection.row_factory = self.dbmi.Row
  836. self.connection.isolation_level = None
  837. self.cursor = self.connection.cursor()
  838. self.cursor.execute("PRAGMA synchronous = OFF")
  839. self.cursor.execute("PRAGMA journal_mode = MEMORY")
  840. elif self.dbmi.__name__ == "psycopg2":
  841. self.connection = self.dbmi.connect(dbstring)
  842. #self.connection.set_isolation_level(dbmi.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
  843. self.cursor = self.connection.cursor(
  844. cursor_factory=self.dbmi.extras.DictCursor)
  845. self.connected = True
  846. except Exception as e:
  847. self.msgr.fatal(_("Unable to connect to %(db)s database: "
  848. "%(string)s\nException: \"%(ex)s\"\nPlease use"
  849. " t.connect to set a read- and writable "
  850. "temporal database backend") % (
  851. {"db": self.dbmi.__name__,
  852. "string": tgis_database_string, "ex": e, }))
  853. def close(self):
  854. """Close the DBMI connection
  855. TODO:
  856. There may be several temporal databases in a location, hence
  857. close all temporal databases that have been opened. Use a dictionary
  858. to manage different connections.
  859. """
  860. self.connection.commit()
  861. self.cursor.close()
  862. self.connected = False
  863. def mogrify_sql_statement(self, content):
  864. """Return the SQL statement and arguments as executable SQL string
  865. TODO:
  866. Use the mapset argument to identify the correct database driver
  867. :param content: The content as tuple with two entries, the first
  868. entry is the SQL statement with DBMI specific
  869. place holder (?), the second entry is the argument
  870. list that should substitute the place holder.
  871. :param mapset: The mapset of the abstract dataset or temporal
  872. database location, if None the current mapset
  873. will be used
  874. Usage:
  875. .. code-block:: python
  876. >>> init()
  877. >>> dbif = SQLDatabaseInterfaceConnection()
  878. >>> dbif.mogrify_sql_statement(["SELECT ctime FROM raster_base WHERE id = ?",
  879. ... ["soil@PERMANENT",]])
  880. "SELECT ctime FROM raster_base WHERE id = 'soil@PERMANENT'"
  881. """
  882. sql = content[0]
  883. args = content[1]
  884. if self.dbmi.__name__ == "psycopg2":
  885. if len(args) == 0:
  886. return sql
  887. else:
  888. if self.connected:
  889. try:
  890. return self.cursor.mogrify(sql, args)
  891. except:
  892. print sql, args
  893. raise
  894. else:
  895. self.connect()
  896. statement = self.cursor.mogrify(sql, args)
  897. self.close()
  898. return statement
  899. elif self.dbmi.__name__ == "sqlite3":
  900. if len(args) == 0:
  901. return sql
  902. else:
  903. # Unfortunately as sqlite does not support
  904. # the transformation of sql strings and qmarked or
  905. # named arguments we must make our hands dirty
  906. # and do it by ourself. :(
  907. # Doors are open for SQL injection because of the
  908. # limited python sqlite3 implementation!!!
  909. pos = 0
  910. count = 0
  911. maxcount = 100
  912. statement = sql
  913. while count < maxcount:
  914. pos = statement.find("?", pos + 1)
  915. if pos == -1:
  916. break
  917. if args[count] is None:
  918. statement = "%sNULL%s" % (statement[0:pos],
  919. statement[pos + 1:])
  920. elif isinstance(args[count], (int, long)):
  921. statement = "%s%d%s" % (statement[0:pos], args[count],
  922. statement[pos + 1:])
  923. elif isinstance(args[count], float):
  924. statement = "%s%f%s" % (statement[0:pos], args[count],
  925. statement[pos + 1:])
  926. else:
  927. # Default is a string, this works for datetime
  928. # objects too
  929. statement = "%s\'%s\'%s" % (statement[0:pos],
  930. str(args[count]),
  931. statement[pos + 1:])
  932. count += 1
  933. return statement
  934. def check_table(self, table_name):
  935. """Check if a table exists in the temporal database
  936. :param table_name: The name of the table to be checked for existence
  937. :param mapset: The mapset of the abstract dataset or temporal
  938. database location, if None the current mapset
  939. will be used
  940. :returns: True if the table exists, False otherwise
  941. TODO:
  942. There may be several temporal databases in a location, hence
  943. the mapset is used to query the correct temporal database.
  944. """
  945. table_exists = False
  946. connected = False
  947. if not self.connected:
  948. self.connect()
  949. connected = True
  950. # Check if the database already exists
  951. if self.dbmi.__name__ == "sqlite3":
  952. self.cursor.execute("SELECT name FROM sqlite_master WHERE "
  953. "type='table' AND name='%s';" % table_name)
  954. name = self.cursor.fetchone()
  955. if name and name[0] == table_name:
  956. table_exists = True
  957. else:
  958. # Check for raster_base table
  959. self.cursor.execute("SELECT EXISTS(SELECT * FROM information_schema.tables "
  960. "WHERE table_name=%s)", ('%s' % table_name,))
  961. if self.cursor.fetchone()[0]:
  962. table_exists = True
  963. if connected:
  964. self.close()
  965. return table_exists
  966. def execute(self, statement, args=None):
  967. """Execute a SQL statement
  968. :param statement: The executable SQL statement or SQL script
  969. """
  970. connected = False
  971. if not self.connected:
  972. self.connect()
  973. connected = True
  974. try:
  975. if args:
  976. self.cursor.execute(statement, args)
  977. else:
  978. self.cursor.execute(statement)
  979. except:
  980. if connected:
  981. self.close()
  982. self.msgr.error(_("Unable to execute :\n %(sql)s" %
  983. {"sql": statement}))
  984. raise
  985. if connected:
  986. self.close()
  987. def fetchone(self):
  988. if self.connected:
  989. return self.cursor.fetchone()
  990. return None
  991. def fetchall(self):
  992. if self.connected:
  993. return self.cursor.fetchall()
  994. return None
  995. def execute_transaction(self, statement, mapset=None):
  996. """Execute a transactional SQL statement
  997. The BEGIN and END TRANSACTION statements will be added automatically
  998. to the sql statement
  999. :param statement: The executable SQL statement or SQL script
  1000. """
  1001. connected = False
  1002. if not self.connected:
  1003. self.connect()
  1004. connected = True
  1005. sql_script = ""
  1006. sql_script += "BEGIN TRANSACTION;\n"
  1007. sql_script += statement
  1008. sql_script += "END TRANSACTION;"
  1009. try:
  1010. if self.dbmi.__name__ == "sqlite3":
  1011. self.cursor.executescript(statement)
  1012. else:
  1013. self.cursor.execute(statement)
  1014. self.connection.commit()
  1015. except:
  1016. if connected:
  1017. self.close()
  1018. self.msgr.error(_("Unable to execute transaction:\n %(sql)s" %
  1019. {"sql": statement}))
  1020. raise
  1021. if connected:
  1022. self.close()
  1023. ###############################################################################
  1024. def init_dbif(dbif):
  1025. """This method checks if the database interface connection exists,
  1026. if not a new one will be created, connected and True will be returned.
  1027. If the database interface exists but is connected, the connection will
  1028. be established.
  1029. :returns: the tuple (dbif, True|False)
  1030. Usage code sample:
  1031. .. code-block:: python
  1032. dbif, connect = tgis.init_dbif(None)
  1033. sql = dbif.mogrify_sql_statement(["SELECT * FROM raster_base WHERE ? = ?"],
  1034. ["id", "soil@PERMANENT"])
  1035. dbif.execute_transaction(sql)
  1036. if connect:
  1037. dbif.close()
  1038. """
  1039. if dbif is None:
  1040. dbif = SQLDatabaseInterfaceConnection()
  1041. dbif.connect()
  1042. return dbif, True
  1043. elif dbif.is_connected() is False:
  1044. dbif.connect()
  1045. return dbif, True
  1046. return dbif, False
  1047. ###############################################################################
  1048. if __name__ == "__main__":
  1049. import doctest
  1050. doctest.testmod()