def _sql (self, fq=True, operation='CREATE', props=None): name = self.name.obj if fq: name = self.name if props: props = InsensitiveDict({prop: expected for prop, (expected, _) in props.items()}) else: props = self.props parts = ["{} SEQUENCE {}".format(operation, name.lower())] if props['increment_by']: parts.append("INCREMENT BY {}".format(props['increment_by'])) if props['maxvalue']: parts.append("MAXVALUE {}".format(props['maxvalue'])) if props['minvalue']: parts.append("MINVALUE {}".format(props['minvalue'])) if props['cycle_flag']: parts.append("{}CYCLE".format( 'NO' if props['cycle_flag'] == 'N' else '')) if props['cache_size']: parts.append("CACHE {}".format(props['cache_size'])) if props['order_flag']: parts.append("{}ORDER".format( 'NO' if props['order_flag'] == 'N' else '')) if 'CREATE' == operation and self.start_with: # START WITH only applies on creation, and can't be validated after. parts.append("START WITH {}".format(self.start_with)) return ' '.join(parts)
class OracleObject (HasLog): @classproperty def type (class_): if hasattr(class_, 'namespace'): class_ = class_.namespace return class_.pretty_type.upper() @classproperty def pretty_type (class_): return re.sub('([A-Z])', r' \1', class_.__name__).strip() def __init__ (self, name, deferred=False, database=None, reinit=False, create_location=None, **props): if not isinstance(name, OracleFQN): name = OracleFQN(obj=name) self.name = name if not reinit: super().__init__() self.create_location = create_location self.deferred = deferred self.database = database self.props = InsensitiveDict(props) self._referenced_by = set() self._dependencies = set() # Ugh this feels hacky self._ignore_name = False def become_deferred (self): self.create_location = None self.deferred = True self.database = None self.props = InsensitiveDict() #self._clear_dependencies() # downgrade back to base types namespace = getattr(type(self), 'namespace', None) if namespace: self.__class__ = namespace def __repr__ (self, **other_props): try: return "<{}>".format(self.pretty_name) except: # When unpickling objects, this can be called before the object is # properly set up, and the above can blow up. This is the fallback. return object.__repr__(self) #if self.deferred: #other_props['deferred'] = True #props = self.props.copy() #props.update(other_props) #return "{}({!r}, {})".format(type(self).__name__, #self.name, #', '.join("{}={!r}".format(k, v) for k, v in props.items())) def __str__ (self): return self.sql() def get_location (self, with_line=True): if not self.create_location: if self.deferred: if self._referenced_by: return "referenced by: {}".format(', '.join( _with_location(ref.from_) for ref in self._referenced_by)) return 'deferred' return 'unknown' parts = ['in "{}"'.format(self.create_location[0])] if with_line and len(self.create_location) > 1: parts.append('line {}'.format(self.create_location[1])) return ', '.join(parts) def __eq__ (self, other): if not isinstance(other, type(self)): return False if not self._ignore_name and self.name != other.name: return False return not bool(self._diff_props(other)) def __ne__ (self, other): return not self == other def __hash__ (self): try: type_self = type(self) if hasattr(type_self, 'namespace'): type_self = type_self.namespace return hash((type_self, self.name)) except: # See __repr__ return object.__hash__(self) @property def pretty_name (self): return " ".join((self.pretty_type, self.name) + ( ('*',) if self.name.generated else ()) + ( (str(id(self)),) if self.log.isEnabledFor(logging.DEBUG) else () )) def sql (self, **args): if not self.deferred: return self._sql(**args) return OracleObject._sql(self, **{'fq': args[k] for k in args if k == 'fq'}) def _sql (self, fq=True): return "-- Placeholder for {}{}".format( 'deferred ' if self.deferred else '', self.pretty_name) @property def sql_produces (self): return {self} def create (self): return Diff(self.sql(), produces=self.sql_produces, priority=Diff.CREATE) def drop (self): self.log.debug("Dropping {}".format(self.pretty_name)) drop = self._drop() ref_diffs = self.teardown() drop.add_dependencies(ref_diffs) return [drop] + ref_diffs def teardown (self): """ Disable or drop all depdendent objects. This is necessary in certain circumstances when modifying an object. """ if self.log.isEnabledFor(logging.DEBUG): self.log.debug("Tearing down {}".format(self.pretty_name)) for ref in self._referenced_by: self.log.debug(ref) return [diff for ref in self._referenced_by if ref.integrity == Reference.HARD for diff in ref.from_.drop()] def build_up (self): diffs = [] for ref in self._referenced_by: if ref.integrity != Reference.SOFT: diffs.append(ref.from_.create()) diffs.extend(ref.from_.build_up()) return diffs def _drop (self): return Diff("DROP {} {}".format(self.type, self.name.lower()), self, priority=Diff.DROP) def recreate (self, other): """ Recreate this object from scratch. Usually means a drop and a create. """ drop, *diffs = other.drop() create = self.create() creates = self.build_up() creates.append(create) for diff in creates: diff.add_dependencies(drop) diffs.append(drop) diffs.extend(creates) return diffs def rebuild (self): """ Try to rebuild this object non-destructively. Used when the object is in an invalid state. If it can't be done non-destructively, it will attempt to recreate the object. """ return self.recreate(self) def rename (self, other): return Diff("ALTER {} {} RENAME TO {}".format(self.type, other.name.lower(), self.name.obj.lower()), produces=self) def satisfy (self, other): if self.deferred: if type(self) is not type(other): self.become(type(other)) if self.name.generated and not other.name.generated: self.name = other.name elif other.name.generated: self.name = _generatify(self.name, other.name) if not self.create_location: self.create_location = other.create_location for k, v in other.props.items(): if (v is not None and k in self.props and self.props[k] is not None and v != self.props[k]): raise PropertyConflict(self, k, v) if k not in self.props or v is not None: self.props[k] = v self._satisfy(other) self.deferred = False def _satisfy (self, other): pass def diff (self, other, recreate=True): """ Calculate differences between self, which is the desired definition, and other, which is the current database state. """ if other.deferred: self.log.warn( "Comparing {!r} to deferred object {!r}".format(self, other)) if self != other: if recreate: return self.recreate(other) elif self.name.obj != other.name.obj: return [self.rename(other)] elif (other.props['status'] and other.props['status'] not in ('VALID', 'N/A')): self.log.debug("{} has status {}".format(other.pretty_name, other.props['status'])) return self.rebuild() return [] def _diff_props (self, other): prop_diff = InsensitiveDict((prop, (expected, other.props[prop])) for prop, expected in self.props.items() if (prop in other.props and expected != other.props[prop])) if self.log.isEnabledFor(logging.DEBUG) and self.name == other.name: for prop in prop_diff: self.log.debug("_diff_props({}, {})[{!r}] expected {!r}, found {!r}" .format(self.pretty_name, other.pretty_name, prop, self.props[prop], other.props[prop])) return prop_diff def diff_subobjects (self, other, get_objects, label=lambda x: x.name, rename=True): diffs = [] targets = get_objects(self) currents = get_objects(other) target_dups = {} if not isinstance(targets, dict): target_objs = {} for obj in targets: key = label(obj) if key in target_objs: target_dups.setdefault(key, []).append(obj) else: target_objs[key] = obj else: target_objs = targets current_dups = {} if not isinstance(currents, dict): current_objs = {} for obj in currents: key = label(obj) if key in current_objs: current_dups.setdefault(key, []).append(obj) else: current_objs[key] = obj else: current_objs = currents ignores = self.database.ignores() | other.database.ignores() filter = lambda d: {name: obj for name, obj in d.items() if (type(obj), str(name)) not in ignores} target_objs = filter(target_objs) current_objs = filter(current_objs) target_obj_names = set(target_objs) current_obj_names = set(current_objs) addobjs = target_obj_names - current_obj_names dropobjs = current_obj_names - target_obj_names # look for potential renames modify_diffs = [] if rename: not_add = set() for add_name in addobjs: add_obj = target_objs[add_name] add_obj._ignore_name = True for drop_name in dropobjs: drop_obj = current_objs[drop_name] if add_obj == drop_obj: modify_diffs.append(add_obj.rename(drop_obj)) not_add.add(add_name) dropobjs.remove(drop_name) break add_obj._ignore_name = False addobjs.difference_update(not_add) if self.log.isEnabledFor(logging.DEBUG): obj_type = ((target_objs and type(next(iter(target_objs.values())))) or (current_objs and type(next(iter(current_objs.values())))) or OracleObject) if hasattr(obj_type, 'namespace'): obj_type = obj_type.namespace pretty_type = obj_type.pretty_type add_dups = [] drop_dups = [] if addobjs: if self.log.isEnabledFor(logging.DEBUG): self.log.debug(" Adding {} {}s: {}".format(len(addobjs), pretty_type, ", ".join(target_objs[obj].pretty_name for obj in addobjs))) objs = [] for addobj in addobjs: if addobj in target_dups: add_dups.append(target_objs[addobj]) add_dups.extend(target_dups[addobj]) else: objs.append(target_objs[addobj]) diffs.extend(self.add_subobjects(objs)) if dropobjs: if self.log.isEnabledFor(logging.DEBUG): self.log.debug(" Dropping {} {}s: {}" .format(len(dropobjs), pretty_type, ", ".join(current_objs[obj].pretty_name for obj in dropobjs))) objs = [] for dropobj in dropobjs: if dropobj in current_dups: drop_dups.append(current_objs[dropobj]) drop_dups.extend(current_dups[dropobj]) else: objs.append(current_objs[dropobj]) diffs.extend(self.drop_subobjects(objs)) for obj_label, target_obj in target_objs.items(): if obj_label in current_objs: try: obj_diffs = target_obj.diff(current_objs[obj_label]) modify_diffs.extend(obj_diffs) dup_diff = (len(target_dups.get(obj_label, [])) - len(current_dups.get(obj_label, []))) if dup_diff > 0: add_dups.extend(target_dups[obj_label][:dup_diff]) elif dup_diff < 0: drop_dups.extend(current_dups[obj_label][:-dup_diff]) except DataConflict as e: self.log.warn(e) if add_dups: diffs.extend(self.add_dup_subobjects(add_dups)) if drop_dups: diffs.extend(self.drop_dup_subobjects(drop_dups)) if modify_diffs and self.log.isEnabledFor(logging.DEBUG): self.log.debug(" {} modifications for {}s".format( len(modify_diffs), pretty_type)) diffs.extend(modify_diffs) return diffs def add_subobjects (self, subobjects): return [obj.create() for obj in subobjects] def drop_subobjects (self, subobjects): return [diff for obj in subobjects for diff in obj.drop()] def add_dup_subobjects (self, subobjects): return self.add_subobjects(subobjects) def drop_dup_subobjects (self, subobjects): return self.drop_subobjects(subobjects) def _depends_on (self, other, prop_name, integrity=Reference.HARD): old_dep = None old_integrity = None if hasattr(self, prop_name): old_dep = getattr(self, prop_name) if old_dep: _old_dep = old_dep if isinstance(_old_dep,OracleObject): _old_dep = [_old_dep] for dep in self._dependencies: if dep.to in _old_dep: old_integrity = dep.integrity break if old_dep is not other or old_integrity != integrity: if old_dep: self._drop_dependency(old_dep) if other: self._set_dependency(other, integrity) setattr(self, prop_name, other) def _set_dependency (self, dep, integrity=Reference.HARD): if isinstance(dep, OracleObject): dep = [dep] if self.log.isEnabledFor(logging.DEBUG): self.log.debug("{} depends {} on [{}]".format(self.pretty_name, integrity, ", ".join(obj.pretty_name for obj in dep))) for obj in dep: ref = Reference(self, obj, integrity) self._dependencies.add(ref) obj._referenced_by.add(ref) def _drop_dependency (self, old_deps): if isinstance(old_deps, OracleObject): old_deps = [old_deps] if self.log.isEnabledFor(logging.DEBUG): self.log.debug("{} no longer depends on [{}]".format(self.pretty_name, ", ".join(old_dep.pretty_name for old_dep in old_deps))) remove_deps = set() for dep in self._dependencies: if dep.to in old_deps: remove_deps.add(dep) remove_refs = set() for ref in dep.to._referenced_by: if ref.from_ is self: remove_refs.add(ref) dep.to._referenced_by.difference_update(remove_refs) self._dependencies.difference_update(remove_deps) def _clear_dependencies (self): if self._dependencies: self._drop_dependency({ref.to for ref in self._dependencies}) def _build_dep_set (self, get_objects, get_ref, test=lambda x: True): all = set() def recurse (_object): for ref in get_objects(_object): if test(ref): obj = get_ref(ref) if obj not in all and obj != self: all.add(obj) recurse(obj) recurse(self) return all def become (self, other_type): name = self.name self.__class__ = other_type self.__init__(name, reinit=True) @property def dependencies (self): return self._build_dep_set(lambda self: self._dependencies, lambda ref: ref.to) def dependencies_with(self, integrity): return self._build_dep_set(lambda self: self._dependencies, lambda ref: ref.to, lambda ref: ref.integrity == integrity) @classmethod def from_db (class_, name=None, into_database=None): raise UnimplementedFeatureError( "Unimplemented from_db for {}".format(class_.__name__))