sources->definition($source); if (! $this->sources->enabled($source)) { throw new RuntimeException('该上游数据库尚未启用。'); } $driver = $this->sources->driver($source); $rows = $this->queries->forSource($source)->select((new UpstreamSqlDialect($driver))->catalogSql()); if ($driver === 'mysql') { foreach ($rows as &$row) { // SQL Server timestamp is binary; MySQL timestamp is temporal. if (strtolower($row['data_type'] ?? '') === 'timestamp') $row['data_type'] = 'datetime'; } unset($row); } if ($rows === []) { throw new RuntimeException('上游数据库没有返回可用的表字段目录。'); } $now = now(); $normalized = $this->normalizeAndDeduplicate($definition['key'], $rows, $now); $objectCount = collect($normalized) ->map(fn (array $row): string => $row['schema_name'] . '.' . $row['object_name']) ->unique() ->count(); DB::transaction(function () use ($definition, $normalized): void { DB::table('upstream_schema_columns') ->where('source_key', $definition['key']) ->delete(); foreach (array_chunk($normalized, 500) as $chunk) { DB::table('upstream_schema_columns')->insert($chunk); } }); return [ 'source' => $definition['key'], 'objects' => $objectCount, 'columns' => count($normalized), 'synchronized_at' => $now, ]; } public function ready(): bool { return Schema::hasTable('upstream_schema_columns'); } private function normalize(string $source, array $row, $now): array { $row = array_change_key_case($row, CASE_LOWER); $schema = trim((string) ($row['schema_name'] ?? '')); $object = trim((string) ($row['object_name'] ?? '')); $column = trim((string) ($row['column_name'] ?? '')); $type = strtolower(trim((string) ($row['object_type'] ?? 'table'))); $dataType = trim((string) ($row['data_type'] ?? '')); if ($schema === '' || $object === '' || $column === '' || $dataType === '') { throw new RuntimeException('上游数据库返回了不完整的字段目录。'); } return [ 'source_key' => $source, 'schema_name' => $schema, 'object_name' => $object, 'object_type' => $type === 'view' ? 'view' : 'table', 'column_name' => $column, 'ordinal_position' => max((int) ($row['ordinal_position'] ?? 0), 1), 'data_type' => $dataType, 'max_length' => $this->nullableInteger($row['max_length'] ?? null), 'numeric_precision' => $this->nullableInteger($row['numeric_precision'] ?? null), 'numeric_scale' => $this->nullableInteger($row['numeric_scale'] ?? null), 'is_nullable' => strtoupper(trim((string) ($row['is_nullable'] ?? 'YES'))) === 'YES', 'synchronized_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; } /** * 部分旧 SQL Server 库的 INFORMATION_SCHEMA 会返回重复字段。 * 共享库的目录键不允许重复,因此写入前按数据库常见的不区分大小写规则去重, * 避免少量脏元数据让整个来源的目录同步回滚。 */ private function normalizeAndDeduplicate(string $source, array $rows, $now): array { $unique = []; foreach ($rows as $row) { $normalized = $this->normalize($source, $row, $now); $identity = implode("\0", [ mb_strtolower($normalized['source_key'], 'UTF-8'), mb_strtolower($normalized['schema_name'], 'UTF-8'), mb_strtolower($normalized['object_name'], 'UTF-8'), mb_strtolower($normalized['column_name'], 'UTF-8'), ]); $unique[$identity] ??= $normalized; } return array_values($unique); } private function nullableInteger(mixed $value): ?int { return $value === null || $value === '' ? null : (int) $value; } }