DuckDB Swift 版介绍

Tristan Celder
2023-04-21 · 5 分钟

要点:DuckDB 现在有了一个原生的 Swift API。DuckDB 可以在移动设备上运行了!

今天,我们很高兴地宣布 DuckDB 的 Swift API。它使 Swift 平台上的开发者能够利用 DuckDB 的全部强大功能,使用具有对诸如强类型和并发等优秀 Swift 功能支持的原生 Swift 接口。该 API 不仅在 Apple 平台上可用,而且在 Linux 上也可用,为不断增长的 Swift 服务器生态系统开辟了新的机会。

包含的内容

DuckDB 的设计宗旨是快速、可靠且易于使用,而这也是我们创建新 Swift API 的指导理念。

这个初始版本开箱即用地支持 DuckDB 的许多优秀功能,包括

  • 通过 DuckDB 增强的 SQL 方言进行查询:除了基本的 SQL 之外,DuckDB 还支持任意和嵌套的相关子查询、窗口函数、排序规则、复杂类型(Swift 数组和结构体)等等。
  • JSON、CSV 和 Parquet 文件的导入和导出:除了其内置的超高效原生文件格式之外,DuckDB 还支持读取和导出到 JSON、CSV 和 Parquet 文件。
  • 强类型结果集:DuckDB 的强类型结果集与 Swift 非常契合。将 DuckDB 列强制转换为其原生 Swift 等效项非常简单,可以准备好使用 SwiftUI 进行演示,或者作为现有 TabularData 工作流的一部分。
  • Swift 并发支持:通过它们的 Sendable 一致性,DuckDB 的许多核心底层类型可以安全地跨并发上下文传递,从而简化了设计并行处理工作流并确保响应式 UI 的过程。

用法

为了演示 DuckDB 与 Swift 的协作效果,我们创建了一个示例项目,该项目使用直接加载到 DuckDB 中的 NASA 系外行星档案中的原始数据。

您将看到如何

  • 实例化一个 DuckDB 内存数据库和连接
  • 使用远程 CSV 的内容填充 DuckDB 表
  • 查询 DuckDB 数据库并准备结果以进行演示

最后,我们将在 Apple 的 TabularData FrameworkSwift Charts 的帮助下展示我们的分析。

实例化 DuckDB

DuckDB 支持基于文件和基于内存的数据库。在此示例中,由于我们不打算将我们的 Exoplanet 分析结果持久化到磁盘,因此我们将选择内存数据库。

let database = try Database(store: .inMemory)

但是,我们还不能发出查询。与其它 RDMS 类似,必须通过数据库连接发出查询。DuckDB 支持每个数据库的多个连接。这对于支持并行处理非常有用。在我们的项目中,我们将只需要一个最终将异步访问的连接。

let connection = try database.connect()

最后,我们将创建一个特定于应用程序的类型,我们将使用它来容纳我们的数据库和连接,并且通过它我们将最终定义我们的特定于应用程序的查询。

import DuckDB

final class ExoplanetStore {

    let database: Database
    let connection: Connection

    init(database: Database, connection: Connection) {
        self.database = database
        self.connection = connection
    }
}

使用远程 CSV 文件填充 DuckDB

我们当前 ExoplanetStore 类型的一个问题是它还没有任何可以查询的数据。为了解决这个问题,我们将从 NASA 的 Exoplanet Archive 中加载迄今为止发现的每个 Exoplanet 的数据。

这个令人难以置信的资源有数百个配置选项,但今天我们想要将每个系外行星的名称及其发现年份打包为 CSV。查看文档,我们得到以下端点

https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=select+pl_name+,+disc_year+from+pscomppars&format=csv

一旦我们在本地下载了我们的 CSV,我们就可以使用以下 SQL 命令将其作为我们 DuckDB 内存数据库中的一个新表加载。DuckDB 的 read_csv_auto 命令会自动推断我们的表模式,并且数据可以立即用于分析。

CREATE TABLE exoplanets AS
    SELECT * FROM read_csv_auto('downloaded_exoplanets.csv'); 

让我们将其打包为我们 ExoplanetStore 类型上的一个新的异步工厂方法

import DuckDB
import Foundation

final class ExoplanetStore {

    // Factory method to create and prepare a new ExoplanetStore
    static func create() async throws -> ExoplanetStore {

        // Create our database and connection as described above
        let database = try Database(store: .inMemory)
        let connection = try database.connect()

        // Download the CSV from the exoplanet archive
        let (csvFileURL, _) = try await URLSession.shared.download(
            from: URL(string: "https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=select+pl_name+,+disc_year+from+pscomppars&format=csv")!)

        // Issue our first query to DuckDB
        try connection.execute("""
            CREATE TABLE exoplanets AS (
                SELECT * FROM read_csv_auto('\(csvFileURL.path)')
            );
            """)

        // Create our pre-populated ExoplanetStore instance
        return ExoplanetStore(
            database: database,
            connection: connection
        )
    }

    // Let's make the initializer we defined previously 
    // private. This prevents anyone accidentally instantiating
    // the store without having pre-loaded our Exoplanet CSV
    // into the database
    private init(database: Database, connection: Connection) {
        // ...
    }
}

查询数据库

现在数据库已经填充了数据,可以进行分析了。让我们创建一个查询,我们可以用它来绘制按年份发现的系外行星数量的图表。

SELECT disc_year, count(disc_year) AS Count
FROM exoplanets
GROUP BY disc_year
ORDER BY disc_year;

从 Swift 中向 DuckDB 发出查询很简单。我们将再次使用一个异步函数来从中发出我们的查询。这意味着在查询执行时,调用者不会被阻塞。然后,我们将使用 DuckDB 的 ResultSet cast(to:) 方法系列将结果列强制转换为 Swift 原生类型,然后将它们包装在来自 TabularData 框架的 DataFrame 中,以便在 UI 中进行演示。

...

import TabularData

extension ExoplanetStore {

    // Retrieves the number of exoplanets discovered by year  
    func groupedByDiscoveryYear() async throws -> DataFrame {

        // Issue the query we described above
        let result = try connection.query("""
            SELECT disc_year, count(disc_year) AS Count
            FROM exoplanets
            GROUP BY disc_year
            ORDER BY disc_year
            """)

        // Cast our DuckDB columns to their native Swift
        // equivalent types
        let discoveryYearColumn = result[0].cast(to: Int.self)
        let countColumn = result[1].cast(to: Int.self)

        // Use our DuckDB columns to instantiate TabularData
        // columns and populate a TabularData DataFrame
        return DataFrame(columns: [
            TabularData.Column(discoveryYearColumn)
                .eraseToAnyColumn(),
            TabularData.Column(countColumn)
                .eraseToAnyColumn(),
        ])
    }
}

可视化结果

只需几行代码,我们的数据库就已经创建、填充和分析完毕了——现在要做的就是展示结果。

我感觉我们才刚刚开始……

对于完整的示例项目 – 包括用于创建上面屏幕截图的 SwiftUI 视图和图表定义 – 克隆 DuckDB Swift 仓库 并打开位于 Examples/SwiftUI/ExoplanetExplorer.xcodeproj 中的可运行的应用程序项目。

我们鼓励您修改代码,探索 Exoplanet Archive 和 DuckDB,并自己进行一些发现 – 星际或其他!

结论

在本文中,我们介绍了 DuckDB 的全新 Swift API,并演示了您可以多么快速地上手分析数据。

凭借 DuckDB 令人难以置信的性能和分析能力,以及 Swift 充满活力的生态系统和平台支持,现在是开始在 Swift 中探索分析数据集的最佳时机。

我们迫不及待地想看看您用它做什么。如果您有任何疑问,请随时在我们的 Discord 上联系我们!


DuckDB 的 Swift API 使用 Swift Package Manager 打包,并位于一个新的顶级存储库 https://github.com/duckdb/duckdb-swift 中。