public function fieldSpecs($classOrInstance, $options = 0)
    {
        $class = ClassInfo::class_name($classOrInstance);

        // Validate options
        if (!is_int($options)) {
            throw new InvalidArgumentException("Invalid options " . var_export($options, true));
        }
        $uninherited = ($options & self::UNINHERITED) === self::UNINHERITED;
        $dbOnly = ($options & self::DB_ONLY) === self::DB_ONLY;
        $includeClass = ($options & self::INCLUDE_CLASS) === self::INCLUDE_CLASS;

        // Walk class hierarchy
        $db = [];
        $classes = $uninherited ? [$class] : ClassInfo::ancestry($class);
        foreach ($classes as $tableClass) {
            // Skip irrelevant parent classes
            if (!is_subclass_of($tableClass, DataObject::class)) {
                continue;
            }

            // Find all fields on this class
            $fields = $this->databaseFields($tableClass, false);
            // Merge with composite fields
            if (!$dbOnly) {
                $compositeFields = $this->compositeFields($tableClass, false);
                $fields = array_merge($fields, $compositeFields);
            }

            // Record specification
            foreach ($fields as $name => $specification) {
                $prefix = $includeClass ? "{$tableClass}." : "";
                $db[$name] = $prefix . $specification;
            }
        }
        return $db;
    }