
# SQL 生成

`SqlGenerator` 会将 `MergeResult` 转换为参数化的 SQL 语句。所有值都作为参数绑定，绝不会插入到 SQL 字符串中。

```php
use Merql\Apply\SqlGenerator;

$statements = SqlGenerator::generate($result);

foreach ($statements as $stmt) {
    echo $stmt['sql'] . "\n";
    // INSERT INTO `posts` (`id`, `title`, `status`) VALUES (?, ?, ?)
    print_r($stmt['params']);
    // [42, 'New Title', 'publish']
}
```

当经过审核的合并计划仅应在实时行仍与预期快照匹配时才应用，请使用 `generateGuarded()`。

```php
use Merql\Apply\SqlGenerator;
use Merql\Snapshot\SnapshotStore;

$statements = SqlGenerator::generateGuarded(
    $selectedResult,
    SnapshotStore::load('theirs'),
);
```

## 语句格式

每条语句都是包含两个键的关联数组：

```php
[
    'sql'    => 'INSERT INTO `posts` (`id`, `title`) VALUES (?, ?)',
    'params' => [42, 'New Title'],
]
```

标识符（表名和列名）使用数据库驱动程序的引用样式进行引用。MySQL 使用反引号，SQLite 使用双引号。未提供驱动程序时，默认使用反引号引用。

## 操作排序

操作按类型分组，并按以下顺序执行：

1. 先执行 **INSERTs** -- 新行必须先存在，更新才能引用它们。
2. 接着执行 **UPDATEs**。
3. 最后执行 **DELETEs** -- 删除之前必须先取消行的引用。

```php
$statements = SqlGenerator::generate($result);
// All INSERTs, then all UPDATEs, then all DELETEs.
```

## 外键排序

提供外键依赖关系时，每组内的操作都会按拓扑顺序排序。插入和更新时先处理父表，再处理子表。删除时先处理子表，再处理父表。

```php
$fkDependencies = [
    'comments' => ['posts'],        // comments references posts
    'post_meta' => ['posts'],       // post_meta references posts
];

$statements = SqlGenerator::generate($result, fkDependencies: $fkDependencies);
// INSERTs: posts first, then comments and post_meta
// DELETEs: comments and post_meta first, then posts
```

`ForeignKeyResolver` 会对依赖关系图执行拓扑排序。它会检测并打破循环依赖，以防止无限循环。

### 自动检测外键

`Applier` 会自动从数据库读取外键关系：

```php
use Merql\Apply\Applier;

$applier = new Applier($connection);
$applied = $applier->apply($result);
// FK dependencies are read from the database via the driver.
```

## 生成的 SQL 示例

### INSERT

```sql
INSERT INTO `posts` (`id`, `title`, `content`, `status`) VALUES (?, ?, ?, ?)
-- params: [42, 'New Post', 'Body text', 'draft']
```

操作值中的所有列都会包含在内。

### UPDATE

```sql
UPDATE `posts` SET `title` = ?, `status` = ? WHERE `id` = ?
-- params: ['New Title', 'publish', 42]
```

标识列（主键）会从 SET 子句中排除，并用于 WHERE 子句。WHERE 子句的值来自对行键的解码。

### DELETE

```sql
DELETE FROM `posts` WHERE `id` = ?
-- params: [42]
```

WHERE 子句中只会出现标识列。

### 带保护条件的 UPDATE

带保护条件的更新同时包含标识列子句和预期实时行子句。

```sql
UPDATE `posts` SET `title` = ? WHERE `id` = ? AND `status` = ?
-- params: ['New Title', 42, 'draft']
```

如果该行不再具有预期的 `status`，语句将影响零行，带保护条件的应用会回滚事务。

### 带保护条件的 INSERT

带保护条件的插入使用 `NOT EXISTS`，以避免插入计划创建后才出现的标识。

```sql
INSERT INTO `posts` (`id`, `title`) SELECT ?, ? WHERE NOT EXISTS (
  SELECT 1 FROM `posts` WHERE `id` = ?
)
-- params: [42, 'New Post', 42]
```

## NULL 处理

NULL 值会作为 PHP `null` 传入 params 数组。连接适配器负责绑定：

```php
$statements = SqlGenerator::generate($result);
// params might contain: [42, null, 'draft']
// The adapter binds null as SQL NULL
```

在试运行输出中，NULL 值会显示为不带引号的字面字符串 `NULL`。

## 标识列检测

`SqlGenerator` 会检查基准快照的 `TableSnapshot::identityColumns`，以确定哪些列构成行标识。这些列用于 UPDATE 和 DELETE 语句的 WHERE 子句，并从 UPDATE 的 SET 子句中排除。

如果没有基准快照，则使用 values 数组中的第一列作为后备值。如果 values 数组为空，则假定为 `id`。

## 传入驱动程序

要控制标识符的引用方式，请传入一个 `Driver` 实例：

```php
use Merql\Driver\SqliteDriver;

$statements = SqlGenerator::generate(
    $result,
    driver: new SqliteDriver(),
);
// Uses double-quote quoting: "posts", "title"
```
