ambuild2/database.py
author David Anderson <dvander@alliedmods.net>
Sun Feb 02 14:00:01 2014 -0800 (2014-02-02)
changeset 235 fd0f5f1a7a10
parent 233 61d5d67d6456
permissions -rw-r--r--
Clean and vacuum unused source nodes after reconfiguring; also don't add nodes for configure files.
     1 # vim: set ts=8 sts=2 sw=2 tw=99 et:
     2 #
     3 # This file is part of AMBuild.
     4 # 
     5 # AMBuild is free software: you can redistribute it and/or modify
     6 # it under the terms of the GNU General Public License as published by
     7 # the Free Software Foundation, either version 3 of the License, or
     8 # (at your option) any later version.
     9 # 
    10 # AMBuild is distributed in the hope that it will be useful,
    11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13 # GNU General Public License for more details.
    14 # 
    15 # You should have received a copy of the GNU General Public License
    16 # along with AMBuild. If not, see <http://www.gnu.org/licenses/>.
    17 import errno
    18 import os, sys
    19 import sqlite3
    20 from ambuild2 import util
    21 from ambuild2 import nodetypes
    22 from ambuild2.nodetypes import Entry
    23 import traceback
    24 
    25 GroupPrefix = '//group/./'
    26 
    27 def CreateDatabase(path):
    28   cn = sqlite3.connect(path)
    29   queries = [
    30     "create table if not exists nodes(            \
    31       id integer primary key autoincrement,       \
    32       type varchar(4) not null,                   \
    33       stamp real not null default 0.0,            \
    34       dirty int not null default 0,               \
    35       path text,                                  \
    36       folder int,                                 \
    37       data blob                                   \
    38     )",
    39 
    40     # The edge table stores links that are specified by the build scripts;
    41     # this table is essentially immutable (except for reconfigures).
    42     "create table if not exists edges(          \
    43       outgoing int not null,                    \
    44       incoming int not null,                    \
    45       unique (outgoing, incoming)               \
    46     )",
    47 
    48     # The weak edge table stores links that are specified by build scripts,
    49     # but only to enforce ordering. They do not propagate damage or updates.
    50     "create table if not exists weak_edges(     \
    51       outgoing int not null,                    \
    52       incoming int not null,                    \
    53       unique (outgoing, incoming)               \
    54     )",
    55 
    56     # The dynamic edge table stores edges that are discovered as a result of
    57     # executing a command; for example, a |cp *| or C++ #includes.
    58     "create table if not exists dynamic_edges(  \
    59       outgoing int not null,                    \
    60       incoming int not null,                    \
    61       unique (outgoing, incoming)               \
    62     )",
    63 
    64     # List of nodes which trigger a reconfigure.
    65     "create table if not exists reconfigure(    \
    66       stamp real not null default 0.0,          \
    67       path text unique                          \
    68     )",
    69 
    70     # Extra key/values we might randomly want.
    71     "create table if not exists vars(           \
    72       key varchar(255) primary key not null,    \
    73       val varchar(255)                          \
    74     )",
    75 
    76     "insert into vars (key, val) values ('db_version', '3')",
    77 
    78     "create index if not exists outgoing_edge on edges(outgoing)",
    79     "create index if not exists incoming_edge on edges(incoming)",
    80     "create index if not exists weak_outgoing_edge on weak_edges(outgoing)",
    81     "create index if not exists weak_incoming_edge on weak_edges(incoming)",
    82     "create index if not exists dyn_outgoing_edge on dynamic_edges(outgoing)",
    83     "create index if not exists dyn_incoming_edge on dynamic_edges(incoming)",
    84 
    85     # The shared output table.
    86     "create table if not exists shared_outputs( \
    87       outgoing int not null,                    \
    88       incoming int not null,                    \
    89       unique (outgoing, incoming)               \
    90     )",
    91     "create index if not exists sho_outgoing_edge on shared_outputs(outgoing)",
    92     "create index if not exists sho_incoming_edge on shared_outputs(incoming)",
    93   ]
    94   for query in queries:
    95     cn.execute(query)
    96   cn.commit()
    97 
    98   db = Database(path)
    99   db.connect()
   100   return db
   101 
   102 class Database(object):
   103   def __init__(self, path):
   104     self.path = path
   105     self.cn = None
   106     self.node_cache_ = {}
   107     self.path_cache_ = {}
   108 
   109   def connect(self):
   110     assert not self.cn
   111     self.cn = sqlite3.connect(self.path)
   112     self.cn.execute("PRAGMA journal_mode = WAL;")
   113     self.check_upgrade()
   114 
   115   def close(self):
   116     if self.cn:
   117       self.cn.close()
   118     self.cn = None
   119 
   120   def __enter__(self):
   121     self.connect()
   122     return self
   123 
   124   def __exit__(self, type, value, traceback):
   125     self.close()
   126 
   127   def commit(self):
   128     self.cn.commit()
   129 
   130   def flush_caches(self):
   131     self.node_cache_ = {}
   132     self.path_cache_ = {}
   133 
   134   def check_upgrade(self):
   135     try:
   136       query = "select val from vars where key = 'db_version'"
   137       cursor = self.cn.execute(query)
   138       row = cursor.fetchone()
   139       if not row:
   140         raise Exception('Database seems to be misconfigured - cannot read version')
   141       version = int(row[0])
   142     except:
   143       version = 1
   144 
   145     latest_version = 3
   146     if version == latest_version:
   147       return
   148     if version > latest_version:
   149       raise Exception('Your database version is too new!')
   150 
   151     util.con_out(
   152       util.ConsoleHeader,
   153       'Note: upgrading database from version {0} to {1}'.format(version, latest_version),
   154       util.ConsoleNormal
   155     )
   156 
   157     if version == 1:
   158       version = self.upgrade_to_v2()
   159 
   160     if version == 2:
   161       version = self.upgrade_to_v3()
   162 
   163   def upgrade_to_v2(self):
   164     queries = [
   165       "create table if not exists vars(           \
   166         key varchar(255) primary key not null,    \
   167         val varchar(255)                          \
   168       )",
   169 
   170       "drop table if exists nodestmp",
   171       "drop table if exists nodesold",
   172 
   173       "create table nodestmp(                       \
   174         id integer primary key autoincrement,       \
   175         type varchar(4) not null,                   \
   176         stamp real not null default 0.0,            \
   177         dirty int not null default 0,               \
   178         path text,                                  \
   179         folder int,                                 \
   180         data blob                                   \
   181       )",
   182     ]
   183     for query in queries:
   184       self.cn.execute(query)
   185 
   186     # Migrate all rows in nodes to nodestmp.
   187     query = """
   188       select id, type, stamp, dirty, path, folder, data
   189         from nodes
   190         order by id asc
   191     """
   192     ins_query = """
   193       insert into nodestmp
   194         (id, type, stamp, dirty, path, folder, data)
   195         values
   196         (?,  ?,    ?,     ?,     ?,    ?,      ?)
   197     """
   198     for row in self.cn.execute(query):
   199       self.cn.execute(ins_query, row)
   200 
   201     self.cn.execute("alter table nodes rename to nodesold")
   202     self.cn.execute("alter table nodestmp rename to nodes")
   203     self.cn.execute("drop table nodesold")
   204     self.cn.execute("insert or replace into vars (key, val) values ('db_version', ?)", (2,))
   205     self.cn.commit()
   206     return 2
   207 
   208   def upgrade_to_v3(self):
   209     queries = [
   210       "create table if not exists shared_outputs( \
   211         outgoing int not null,                    \
   212         incoming int not null,                    \
   213         unique (outgoing, incoming)               \
   214       )",
   215       "create index if not exists sho_outgoing_edge on shared_outputs(outgoing)",
   216       "create index if not exists sho_incoming_edge on shared_outputs(incoming)",
   217     ]
   218     for query in queries:
   219       self.cn.execute(query)
   220     self.cn.execute("insert or replace into vars (key, val) values ('db_version', ?)", (3,))
   221     self.cn.commit()
   222     return 3
   223 
   224   def add_folder(self, parent, path):
   225     assert path not in self.path_cache_
   226     assert not os.path.isabs(path)
   227     assert os.path.normpath(path) == path
   228 
   229     return self.add_file(nodetypes.Mkdir, path, parent)
   230 
   231   def add_output(self, folder_entry, path, kind = nodetypes.Output):
   232     assert path not in self.path_cache_
   233     assert not os.path.isabs(path)
   234     assert not folder_entry or os.path.split(path)[0] == folder_entry.path
   235     assert kind == nodetypes.Output or kind == nodetypes.SharedOutput
   236 
   237     return self.add_file(kind, path, folder_entry)
   238 
   239   def find_or_add_source(self, path):
   240     node = self.query_path(path)
   241     if node:
   242       assert node.type == nodetypes.Source
   243       return node
   244 
   245     return self.add_source(path)
   246 
   247   def add_source(self, path):
   248     assert path not in self.path_cache_
   249     assert os.path.isabs(path)
   250 
   251     return self.add_file(nodetypes.Source, path)
   252 
   253   def add_file(self, type, path, folder_entry = None):
   254     if folder_entry:
   255       folder_id = folder_entry.id
   256     else:
   257       folder_id = None
   258 
   259     query = "insert into nodes (type, path, folder) values (?, ?, ?)"
   260 
   261     cursor = self.cn.execute(query, (type, path, folder_id))
   262     row = (type, 0, 1, path, folder_entry, None)
   263     return self.import_node(
   264       id=cursor.lastrowid,
   265       row=row
   266     )
   267 
   268   def find_group(self, name):
   269     path = GroupPrefix + name
   270     return self.query_path(path)
   271 
   272   def add_group(self, name):
   273     path = GroupPrefix + name
   274     return self.add_file(nodetypes.Group, path, False)
   275 
   276   def update_command(self, entry, type, folder, data, refactoring):
   277     if not data:
   278       blob = None
   279     else:
   280       blob = util.BlobType(util.CompatPickle(data))
   281 
   282     if entry.type == type and entry.folder == folder and entry.blob == data:
   283       return False
   284 
   285     if refactoring:
   286       util.con_err(util.ConsoleRed, 'Command changed! \n',
   287                    util.ConsoleRed, 'Old: ',
   288                    util.ConsoleBlue, entry.format(),
   289                    util.ConsoleNormal)
   290       entry.type = type
   291       entry.folder = folder
   292       entry.blob = data
   293       util.con_err(util.ConsoleRed, 'New: ',
   294                    util.ConsoleBlue, entry.format(),
   295                    util.ConsoleNormal)
   296       raise Exception('Refactoring error: command changed')
   297 
   298     if not folder:
   299       folder_id = None
   300     else:
   301       folder_id = folder.id
   302 
   303     query = """
   304       update nodes
   305       set
   306         type = ?,
   307         folder = ?,
   308         data = ?,
   309         dirty = ?
   310       where id = ?
   311     """
   312     self.cn.execute(query, (type, folder_id, blob, 1, entry.id))
   313     entry.type = type
   314     entry.folder = folder
   315     entry.blob = blob
   316     entry.dirty = True
   317     return True
   318 
   319   def add_command(self, type, folder, data):
   320     if not data:
   321       blob = None
   322     else:
   323       blob = util.BlobType(util.CompatPickle(data))
   324     if not folder:
   325       folder_id = None
   326     else:
   327       folder_id = folder.id
   328 
   329     query = "insert into nodes (type, folder, data, dirty) values (?, ?, ?, ?)"
   330     cursor = self.cn.execute(query, (type, folder_id, blob, 1))
   331 
   332     entry = Entry(
   333       id=cursor.lastrowid,
   334       type=type,
   335       path=None,
   336       blob=data,
   337       folder=folder,
   338       stamp=0,
   339       dirty=True
   340     )
   341     self.node_cache_[entry.id] = entry
   342     return entry
   343 
   344   def add_weak_edge(self, from_entry, to_entry):
   345     query = "insert into weak_edges (outgoing, incoming) values (?, ?)"
   346     self.cn.execute(query, (to_entry.id, from_entry.id))
   347     if to_entry.weak_inputs is not None:
   348       to_entry.weak_inputs.add(from_entry)
   349 
   350   def add_strong_edge(self, from_entry, to_entry):
   351     query = "insert into edges (outgoing, incoming) values (?, ?)"
   352     self.cn.execute(query, (to_entry.id, from_entry.id))
   353     if to_entry.strong_inputs is not None:
   354       to_entry.strong_inputs.add(from_entry)
   355     if from_entry.outgoing is not None:
   356       from_entry.outgoing.add(to_entry)
   357 
   358   def add_dynamic_edge(self, from_entry, to_entry):
   359     query = "insert into dynamic_edges (outgoing, incoming) values (?, ?)"
   360     self.cn.execute(query, (to_entry.id, from_entry.id))
   361     if to_entry.dynamic_inputs is not None:
   362       to_entry.dynamic_inputs.add(from_entry)
   363     if from_entry.outgoing is not None:
   364       from_entry.outgoing.add(to_entry)
   365 
   366   def add_shared_output_edge(self, from_entry, to_entry):
   367     # These don't factor into the DAG in any meaningful way, so we don't
   368     # cache the results or put them into edge lists.
   369     query = "insert into shared_outputs (outgoing, incoming) values (?, ?)"
   370     self.cn.execute(query, (to_entry.id, from_entry.id))
   371 
   372   def drop_dynamic_edge(self, from_entry, to_entry):
   373     query = "delete from dynamic_edges where outgoing = ? and incoming = ?"
   374     self.cn.execute(query, (to_entry.id, from_entry.id))
   375     if to_entry.dynamic_inputs is not None:
   376       to_entry.dynamic_inputs.remove(from_entry)
   377     if from_entry.outgoing is not None:
   378       from_entry.outgoing.remove(to_entry)
   379 
   380   def drop_weak_edge(self, from_entry, to_entry):
   381     query = "delete from weak_edges where outgoing = ? and incoming = ?"
   382     self.cn.execute(query, (to_entry.id, from_entry.id))
   383     if to_entry.weak_inputs is not None:
   384       to_entry.weak_inputs.remove(from_entry)
   385 
   386   def drop_strong_edge(self, from_entry, to_entry):
   387     query = "delete from edges where outgoing = ? and incoming = ?"
   388     self.cn.execute(query, (to_entry.id, from_entry.id))
   389     if to_entry.strong_inputs is not None:
   390       to_entry.strong_inputs.remove(from_entry)
   391     if from_entry.outgoing is not None:
   392       from_entry.outgoing.remove(to_entry)
   393 
   394   def drop_shared_output_edge(self, from_entry, to_entry):
   395     query = "delete from shared_outputs where outgoing = ? and incoming = ?"
   396     self.cn.execute(query, (to_entry.id, from_entry.id))
   397 
   398   def query_node(self, id):
   399     if id in self.node_cache_:
   400       return self.node_cache_[id]
   401 
   402     query = "select type, stamp, dirty, path, folder, data from nodes where id = ?"
   403     cursor = self.cn.execute(query, (id,))
   404     return self.import_node(id, cursor.fetchone())
   405 
   406   def query_path(self, path):
   407     if path in self.path_cache_:
   408       return self.path_cache_[path]
   409 
   410     query = """
   411       select type, stamp, dirty, path, folder, data, id
   412       from nodes
   413       where path = ?
   414     """
   415     cursor = self.cn.execute(query, (path,))
   416     row = cursor.fetchone()
   417     if not row:
   418       return None
   419 
   420     return self.import_node(row[6], row)
   421 
   422   def import_node(self, id, row):
   423     assert id not in self.node_cache_
   424 
   425     if not row[4]:
   426       folder = None
   427     elif type(row[4]) is nodetypes.Entry:
   428       folder = row[4]
   429     else:
   430       folder = self.query_node(row[4])
   431     if not row[5]:
   432       blob = None
   433     else:
   434       blob = util.Unpickle(row[5])
   435 
   436     node = Entry(id=id,
   437                  type=row[0],
   438                  path=row[3],
   439                  blob=blob,
   440                  folder=folder,
   441                  stamp=row[1],
   442                  dirty=row[2])
   443     self.node_cache_[id] = node
   444     if node.path:
   445       assert node.path not in self.path_cache_
   446       self.path_cache_[node.path] = node
   447     return node
   448 
   449   def query_strong_outgoing(self, node):
   450     # Not cached yet.
   451     outgoing = set()
   452     query = "select outgoing from edges where incoming = ?"
   453     for outgoing_id, in self.cn.execute(query, (node.id,)):
   454       entry = self.query_node(outgoing_id)
   455       outgoing.add(entry)
   456     return outgoing
   457 
   458   # Find the list of shared outputs this command generates.
   459   def query_shared_outputs(self, node):
   460     # Not cached.
   461     outgoing = set()
   462     query = "select outgoing from shared_outputs where incoming = ?"
   463     for outgoing_id, in self.cn.execute(query, (node.id,)):
   464       entry = self.query_node(outgoing_id)
   465       outgoing.add(entry)
   466     return outgoing
   467 
   468   def query_outgoing(self, node):
   469     if node.outgoing is not None:
   470       return node.outgoing
   471 
   472     node.outgoing = set()
   473 
   474     query = "select outgoing from edges where incoming = ?"
   475     for outgoing_id, in self.cn.execute(query, (node.id,)):
   476       entry = self.query_node(outgoing_id)
   477       node.outgoing.add(entry)
   478 
   479     query = "select outgoing from dynamic_edges where incoming = ?"
   480     for outgoing_id, in self.cn.execute(query, (node.id,)):
   481       entry = self.query_node(outgoing_id)
   482       node.outgoing.add(entry)
   483 
   484     return node.outgoing
   485 
   486   def query_weak_inputs(self, node):
   487     if node.weak_inputs is not None:
   488       return node.weak_inputs
   489 
   490     query = "select incoming from weak_edges where outgoing = ?"
   491     node.weak_inputs = set()
   492     for incoming_id, in self.cn.execute(query, (node.id,)):
   493       incoming = self.query_node(incoming_id)
   494       node.weak_inputs.add(incoming)
   495 
   496     return node.weak_inputs
   497 
   498   def query_strong_inputs(self, node):
   499     if node.strong_inputs is not None:
   500       return node.strong_inputs
   501 
   502     query = "select incoming from edges where outgoing = ?"
   503     node.strong_inputs = set()
   504     for incoming_id, in self.cn.execute(query, (node.id,)):
   505       incoming = self.query_node(incoming_id)
   506       node.strong_inputs.add(incoming)
   507 
   508     return node.strong_inputs
   509 
   510   def query_command_of(self, node):
   511     assert node.type == nodetypes.Output
   512     incoming = self.query_strong_inputs(node)
   513 
   514     assert len(incoming) <= 1
   515     if not len(incoming):
   516       return None
   517 
   518     cmd_entry = list(incoming)[0]
   519     assert cmd_entry.isCommand()
   520 
   521     return cmd_entry
   522 
   523   def query_shared_commands_of(self, node):
   524     query = "select incoming from shared_outputs where outgoing = ?"
   525     commands = []
   526     for row in self.cn.execute(query, (node.id,)):
   527       commands.append(self.query_node(row[0]))
   528     return commands
   529 
   530   def query_dynamic_inputs(self, node):
   531     if node.dynamic_inputs is not None:
   532       return node.dynamic_inputs
   533 
   534     query = "select incoming from dynamic_edges where outgoing = ?"
   535     node.dynamic_inputs = set()
   536     for incoming_id, in self.cn.execute(query, (node.id,)):
   537       incoming = self.query_node(incoming_id)
   538       node.dynamic_inputs.add(incoming)
   539 
   540     return node.dynamic_inputs
   541 
   542   def mark_dirty(self, entry):
   543     query = "update nodes set dirty = 1 where id = ?"
   544     self.cn.execute(query, (entry.id,))
   545     entry.dirty |= nodetypes.KnownDirty
   546 
   547   def unmark_dirty(self, entry, stamp=None):
   548     query = "update nodes set dirty = 0, stamp = ? where id = ?"
   549     if not stamp:
   550       if entry.isCommand():
   551         stamp = 0.0
   552       else:
   553         try:
   554           stamp = os.path.getmtime(entry.path)
   555         except:
   556           traceback.print_exc()
   557           util.con_err(
   558             util.ConsoleRed,
   559             'Could not unmark file as dirty; leaving dirty.',
   560             util.ConsoleNormal
   561           )
   562           return
   563 
   564     self.cn.execute(query, (stamp, entry.id))
   565     entry.dirty = False
   566     entry.stamp = stamp
   567 
   568   # Query all mkdir nodes.
   569   def query_mkdir(self, aggregate):
   570     query = """
   571       select type, stamp, dirty, path, folder, data, id
   572       from nodes
   573       where type == 'mkd'
   574     """
   575     for row in self.cn.execute(query):
   576       id = row[6]
   577       node = self.import_node(id, row)
   578       aggregate(node)
   579 
   580   # Intended to be called before any nodes are imported.
   581   def query_known_dirty(self, aggregate):
   582     query = """
   583       select type, stamp, dirty, path, folder, data, id
   584       from nodes
   585       where dirty = 1
   586       and type != 'mkd'
   587     """
   588     for row in self.cn.execute(query):
   589       id = row[6]
   590       node = self.import_node(id, row)
   591       aggregate(node)
   592 
   593   # Query all nodes that are not dirty, but need to be checked. Intended to
   594   # be called after query_dirty, and returns a mutually exclusive list.
   595   def query_maybe_dirty(self, aggregate):
   596     query = """
   597       select type, stamp, dirty, path, folder, data, id
   598       from nodes
   599       where dirty = 0
   600       and (type == 'src' or type == 'out' or type == 'cpa')
   601     """
   602     for row in self.cn.execute(query):
   603       id = row[6]
   604       node = self.import_node(id, row)
   605       aggregate(node)
   606 
   607   # Find the list of commands that generate this shared output.
   608   def query_commands(self, aggregate):
   609     query = """
   610       select type, stamp, dirty, path, folder, data, id
   611       from nodes
   612       where (type != 'src' and
   613              type != 'out' and
   614              type != 'sho' and
   615              type != 'grp' and
   616              type != 'mkd')
   617     """
   618     for row in self.cn.execute(query):
   619       id = row[6]
   620       node = self.import_node(id, row)
   621       aggregate(node)
   622 
   623   # Note that this does not update any caches. It should only be called
   624   # around cleanup.
   625   def drop_links(self, entry):
   626     query = "delete from edges where incoming = ? or outgoing = ?"
   627     self.cn.execute(query, (entry.id, entry.id))
   628 
   629     query = "delete from dynamic_edges where incoming = ? or outgoing = ?"
   630     self.cn.execute(query, (entry.id, entry.id))
   631 
   632     query = "delete from weak_edges where incoming = ? or outgoing = ?"
   633     self.cn.execute(query, (entry.id, entry.id))
   634 
   635   def drop_entry(self, entry):
   636     self.drop_links(entry)
   637 
   638     query = "delete from nodes where id = ?"
   639     self.cn.execute(query, (entry.id,))
   640 
   641     del self.node_cache_[entry.id]
   642 
   643   def drop_folder(self, entry):
   644     assert entry.type in [nodetypes.Mkdir, nodetypes.Output, nodetypes.SharedOutput]
   645     assert not os.path.isabs(entry.path)
   646 
   647     if os.path.exists(entry.path):
   648       util.con_out(
   649         util.ConsoleHeader, 'Removing old folder: ',
   650         util.ConsoleBlue, '{0}'.format(entry.path),
   651         util.ConsoleNormal)
   652 
   653     try:
   654       os.rmdir(entry.path)
   655     except OSError as exn:
   656       if exn.errno != errno.ENOENT:
   657         util.con_err(util.ConsoleRed, 'Could not remove folder: ',
   658                      util.ConsoleBlue, '{0}'.format(entry.path),
   659                      util.ConsoleNormal, '\n',
   660                      util.ConsoleRed, '{0}'.format(exn),
   661                      util.ConsoleNormal)
   662         raise
   663 
   664     cursor = self.cn.execute("select count(*) from nodes where folder = ?", (entry.id,))
   665     amount = cursor.fetchone()[0]
   666     if amount > 0:
   667       util.con_err(util.ConsoleRed, 'Folder id ',
   668                    util.ConsoleBlue, '{0} '.format(entry.id),
   669                    util.ConsoleRed, 'is about to be deleted, but is still in use as a folder!',
   670                    util.ConsoleNormal)
   671       raise Exception('folder still in use!')
   672 
   673     # If the node transitioned to an entry, don't delete its node.
   674     if entry.type == nodetypes.Mkdir:
   675       self.drop_entry(entry)
   676 
   677   def drop_source(self, output):
   678     assert output.type == nodetypes.Source
   679     self.drop_entry(output)
   680 
   681   def drop_output(self, output):
   682     assert output.type == nodetypes.Output or output.type == nodetypes.SharedOutput
   683     util.rm_path(output.path)
   684     self.drop_entry(output)
   685 
   686   def drop_command(self, cmd_entry):
   687     for output in self.query_outgoing(cmd_entry):
   688       # Commands should never have dynamic outgoing edges, FWIW.
   689       assert output.type == nodetypes.Output
   690       self.drop_output(output)
   691     self.cn.execute("delete from shared_outputs where incoming = ?", (cmd_entry.id,))
   692     self.drop_entry(cmd_entry)
   693 
   694   def add_or_update_script(self, path):
   695     stamp = os.path.getmtime(path)
   696     query = "insert or replace into reconfigure (path, stamp) values (?, ?)"
   697     self.cn.execute(query, (path, stamp))
   698 
   699   def query_scripts(self, aggregate):
   700     query = "select rowid, path, stamp from reconfigure"
   701     for rowid, path, stamp in self.cn.execute(query):
   702       aggregate(rowid, path, stamp)
   703 
   704   def query_groups(self, aggregate):
   705     query = """
   706       select type, stamp, dirty, path, folder, data, id
   707       from nodes
   708       where type == 'grp'
   709     """
   710     for row in self.cn.execute(query):
   711       id = row[6]
   712       entry = self.import_node(id, row)
   713       aggregate(entry)
   714 
   715   def query_dead_sources(self, aggregate):
   716     query = """
   717       select id from nodes
   718         where type == '{0}'
   719         and id not in (select incoming from edges)
   720         and id not in (select incoming from dynamic_edges)
   721         and id not in (select incoming from weak_edges)
   722     """.format(nodetypes.Source)
   723 
   724     for row in self.cn.execute(query):
   725       entry = self.query_node(row[0])
   726       aggregate(entry)
   727 
   728   def query_dead_shared_outputs(self, aggregate):
   729     query = """
   730       select id from nodes
   731         where type == '{0}'
   732         and id not in (select outgoing from shared_outputs)
   733     """.format(nodetypes.SharedOutput)
   734 
   735     for row in self.cn.execute(query):
   736       entry = self.query_node(row[0])
   737       aggregate(entry)
   738 
   739   def drop_script(self, path):
   740     self.cn.execute("delete from reconfigure where path = ?", (path,))
   741 
   742   def drop_group(self, group):
   743     self.drop_entry(group)
   744 
   745   def change_to_folder(self, entry):
   746     assert entry.type == nodetypes.Output or entry.type == nodetypes.SharedOutput
   747     self.drop_links(entry)
   748     self.cn.execute("update nodes set type = 'mkd' where id = ?", (entry.id,))
   749     entry.type = nodetypes.Mkdir
   750 
   751   def change_to_output(self, entry, kind):
   752     assert entry.type in [nodetypes.Mkdir, nodetypes.Output, nodetypes.SharedOutput]
   753     self.drop_links(entry)
   754     self.cn.execute("update nodes set type = ? where id = ?", (kind, entry.id))
   755     entry.type = kind
   756 
   757   def vacuum(self):
   758     self.cn.execute("vacuum")
   759 
   760   def printGraph(self):
   761     # Find all mkdir nodes.
   762     query = "select path from nodes where type = 'mkd'"
   763     for path, in self.cn.execute(query):
   764       print(' : mkdir \"' + path + '\"')
   765     # Find all other nodes that have no outgoing edges.
   766     query = "select id from nodes where id not in (select incoming from edges) and type != 'mkd'"
   767     for id, in self.cn.execute(query):
   768       node = self.query_node(id)
   769       self.printGraphNode(node, 0)
   770 
   771   def printGraphNode(self, node, indent):
   772     print(('  ' * indent) + ' - ' + node.format())
   773 
   774     for incoming in self.query_strong_inputs(node):
   775       self.printGraphNode(incoming, indent + 1)
   776     for incoming in self.query_dynamic_inputs(node):
   777       self.printGraphNode(incoming, indent + 1)