should be it
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/*:
|
||||
[Home](1.%20Welcome)
|
||||
# Welcome to DuckDB
|
||||
|
||||
## Introduction
|
||||
DuckDB is a high-performance analytical database system. It is designed to be
|
||||
fast, reliable and easy to use. DuckDB provides a rich SQL dialect, with
|
||||
support far beyond basic SQL. DuckDB supports arbitrary and nested correlated
|
||||
subqueries, window functions, collations, complex types (arrays, structs), and
|
||||
more. For more information on the goals of DuckDB, please refer to the [Why
|
||||
DuckDB page](https://duckdb.org/why_duckdb) on our website.
|
||||
|
||||
## How to use these playgrounds
|
||||
Each playground is a live code example that you can run and edit to familiarise
|
||||
yourself with the DuckDB Swift API. Playgrounds should run automatically, but
|
||||
if your environment is set-up to run manually, press the play button in the
|
||||
debug window to get started.
|
||||
|
||||
> For playgrounds in this series to work, it's important to access them via the
|
||||
> `DuckDB.xcworkspace` file otherwise Xcode Playgrounds will be unable to find
|
||||
> the DuckDB Swift module.
|
||||
|
||||
## Table of Contents
|
||||
1. [Welcome to DuckDB](1.%20Welcome) (This page)
|
||||
2. [Getting Started](2.%20Getting%20Started)
|
||||
3. [Visualizing Data](3.%20Visualizing%20Data)
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,162 @@
|
||||
/*:
|
||||
[Home](1.%20Welcome)
|
||||
# Getting Started
|
||||
|
||||
## Introduction
|
||||
|
||||
In this first playground, we'll show you how you can use DuckDB with SwiftUI
|
||||
to view a table of data. The task is simple, but it will demonstrate many of
|
||||
the steps you'll need to interact successfully with DuckDB.
|
||||
|
||||
> For this playground to work, it's important to access it via the
|
||||
> `DuckDB.xcworkspace` file otherwise Xcode Playgrounds will be unable to find
|
||||
> the DuckDB Swift module.
|
||||
|
||||
You'll see how to:
|
||||
- Create an in-memory database
|
||||
- Create a connection to that database
|
||||
- Query the database through the connection
|
||||
|
||||
We'll also be showing how DuckDB can be used to not only query internal tables,
|
||||
but also any CSV or JSON files that you may have downloaded to your file system.
|
||||
|
||||
So let's get started.
|
||||
*/
|
||||
|
||||
PlaygroundPage.current.needsIndefiniteExecution = true
|
||||
|
||||
// First, we need to import the DuckDB modules plus any other modules we'll be
|
||||
// using within this playground
|
||||
|
||||
import DuckDB
|
||||
import PlaygroundSupport
|
||||
import SwiftUI
|
||||
|
||||
/*:
|
||||
## Step 1: Fetching Data
|
||||
|
||||
Before we can query data, we'll need some data to query.
|
||||
|
||||
In this example, and for the other playground pages in this series, we're using
|
||||
the [Stack Overflow Annual Developer Survey](https://insights.stackoverflow.com/survey).
|
||||
In the 2022 survey there were over 70,000 responses from 180 countries and
|
||||
each and every response has been collected into a CSV for us to analyze.
|
||||
|
||||
Handy. But what's in it?
|
||||
|
||||
That seems like a great first task. Let's create a simple query that allows us
|
||||
to see all the columns in the table – plus they're types – so we know what
|
||||
we're working with. We'll use SwiftUI to visualize our results in a simple
|
||||
table.
|
||||
*/
|
||||
|
||||
// Now the real-stuff. we'll define an asynchronous function that performs
|
||||
// the heavy-lifting in extracting our required data
|
||||
|
||||
func fetchData() async throws -> ResultSet {
|
||||
|
||||
// Here, we download the CSVs for the Stack Overflow survey. Subsequent calls
|
||||
// to this method return a cached version you can view at
|
||||
// `~/Documents/Shared Playground Data/org.duckdb/surveys`
|
||||
let fileURL = try await SurveyLoader.downloadSurveyCSV(forYear: 2022)
|
||||
|
||||
// We'll use an in-memory store for our database, as we don't need to persist
|
||||
// anything
|
||||
let database = try Database(store: .inMemory)
|
||||
|
||||
// Next we create a connection. A connection is used to issue queries to the
|
||||
// database. You can have multiple connections per database.
|
||||
let connection = try database.connect()
|
||||
|
||||
// We're now ready to issue our first query! Notice how we enclose the file
|
||||
// path for the CSV in single quotes. We'll limit this to zero as we're only
|
||||
// interested in the column layout for now.
|
||||
return try connection.query("SELECT * FROM '\(fileURL.path)' LIMIT 0")
|
||||
}
|
||||
|
||||
/*:
|
||||
|
||||
## Step 2: Rendering Data
|
||||
|
||||
With our asynchronous data fetching function in hand, we can move on to
|
||||
displaying the data.
|
||||
|
||||
We'll define a view with a single `rows` property initially set to `nil`. Then,
|
||||
when the view loads we'll call the function we defined in Step 1 to load the
|
||||
data asynchronously.
|
||||
|
||||
SwiftUI's `task(_:)` modifier is perfect for this. The `task(_:)` modifier runs
|
||||
in the background when the view is loaded. We'll attach the `task(_:)`
|
||||
modifier to our loading spinner so we can see that something is happening.
|
||||
|
||||
Finally, we'll process the result into rows for our view table and set the
|
||||
`rows` property. This will act as SwiftUI's trigger to re-render the view.
|
||||
*/
|
||||
|
||||
struct TableLayoutView: View {
|
||||
|
||||
// We'll define a new type to hold the view data for our table view
|
||||
struct TableRow: Identifiable {
|
||||
let id: String
|
||||
let databaseType: String
|
||||
}
|
||||
|
||||
// This will hold our view state and signal to the view whether or not it
|
||||
// can show our gathered data
|
||||
enum ViewState {
|
||||
case loading
|
||||
case result(ResultSet)
|
||||
}
|
||||
|
||||
@State var state = ViewState.loading
|
||||
|
||||
var body: some View {
|
||||
switch state {
|
||||
|
||||
// Whilst we're waiting for our data to load we'll display a spinner
|
||||
case .loading:
|
||||
ProgressView { Text("loading") }
|
||||
.task {
|
||||
do {
|
||||
// Here, we kick-off the function we defined in step 1
|
||||
self.state = .result(try await fetchData())
|
||||
}
|
||||
catch { print("error: \(error)") }
|
||||
}
|
||||
|
||||
// Now our result set has been loaded we can display its data
|
||||
case .result(let result):
|
||||
Table(result) {
|
||||
TableColumn("Column Name", value: \.name)
|
||||
TableColumn("Column Type", value: \.typeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plus a small extension on Column to help us get the column type as a String
|
||||
fileprivate extension Column {
|
||||
var typeName: String {
|
||||
String(describing: underlyingDatabaseType)
|
||||
}
|
||||
}
|
||||
|
||||
/*:
|
||||
|
||||
## Conclusion
|
||||
|
||||
And that's how easy it is to get started with DuckDB.
|
||||
|
||||
You've learnt how to create a database, connect to it, and issue queries.
|
||||
|
||||
Next-up, we'll see how we can use DuckDB to create incredible visulizations
|
||||
using SwiftUI, TabularData and SwiftCharts. See you there.
|
||||
|
||||
[Next Playground](@next)
|
||||
*/
|
||||
|
||||
// Kick-off the playground
|
||||
PlaygroundPage.current.setLiveView(
|
||||
TableLayoutView()
|
||||
.frame(width: 640, height: 480, alignment: .center)
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Timeline
|
||||
version = "3.0">
|
||||
<TimelineItems>
|
||||
</TimelineItems>
|
||||
</Timeline>
|
||||
@@ -0,0 +1,190 @@
|
||||
/*:
|
||||
[Home](1.%20Welcome)
|
||||
# Visualizing Data
|
||||
|
||||
## Introduction
|
||||
|
||||
In this playground, we build on the skills learnt in
|
||||
[Getting Started](2.%20Getting%20Started) and begin to extract some
|
||||
meaningful data from our data source.
|
||||
|
||||
> For this playground to work, it's important to access it via the
|
||||
> `DuckDB.xcworkspace` file otherwise Xcode Playgrounds will be unable to find
|
||||
> the DuckDB Swift module.
|
||||
|
||||
You'll see how to:
|
||||
- Cast DuckDB columns to native Swift types
|
||||
- Visualize extracted data using Swift Charts
|
||||
*/
|
||||
|
||||
PlaygroundPage.current.needsIndefiniteExecution = true
|
||||
|
||||
// Import the DuckDB modules plus any other modules we'll be using within
|
||||
// this playground
|
||||
|
||||
import DuckDB
|
||||
import PlaygroundSupport
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
/*:
|
||||
## Step 1: Extracting Data
|
||||
|
||||
Thanks to our work in the [Getting Started](2.%20Getting%20Started) playground,
|
||||
we can start now start to do some meaningful analysis on our data.
|
||||
|
||||
In this example we're going to take a look at which countries generated the
|
||||
largest response in the
|
||||
[Stack Overflow Annual Developer Survey](https://insights.stackoverflow.com/survey)
|
||||
and plot the results into a chart.
|
||||
|
||||
Let's create a function that extracts the top 10 countries participating in
|
||||
the survey and puts it into a format ready for visualization.
|
||||
|
||||
### Casting columns
|
||||
|
||||
To get the data from our columns out, we'll use the `cast(to:)` method on
|
||||
Column. It's important that you only cast to a column that matches the
|
||||
`underlyingDataType` of the Column you are casting from, or you'll get an
|
||||
assert in debug builds and nil values in release mode.
|
||||
|
||||
Its possible to debug the column layout – and therefore column types – of any
|
||||
ResultSet object by simply printing it to the console, or checking the
|
||||
`underlyingDataType` member of the Column.
|
||||
|
||||
Once you know this, you can then select the appropriate Swift type to cast to.
|
||||
For example, a Column with a `underlyingDatabaseType` of `DatabaseType.varchar`
|
||||
casts to `String`, and a column with an underlying database type of
|
||||
`DatabaseType.bigint` casts to `Int32` – and for convenience `Int`. For DuckDB
|
||||
composite types you can even cast to matching types which conform to
|
||||
`Decodable`.
|
||||
|
||||
You can see which database types convert to which Swift types by checking out
|
||||
the docs for `DatabaseType`.
|
||||
|
||||
Let's see this in practice.
|
||||
*/
|
||||
|
||||
// We'll use this type as a source for our chart
|
||||
struct ChartRow {
|
||||
let nameOfCountry: String
|
||||
let participantCount: Int
|
||||
}
|
||||
|
||||
// It needs to conform to identifiable so that SwiftUI can group it correctly
|
||||
extension ChartRow: Identifiable {
|
||||
var id: String { nameOfCountry }
|
||||
}
|
||||
|
||||
// Here, we'll define an asynchronous function for extracting our data.
|
||||
func generateSurveyParticipantCountryChartData() async throws -> [ChartRow] {
|
||||
|
||||
// We'll connect to the database in the exact same way as the previous
|
||||
// playground
|
||||
let fileURL = try await SurveyLoader.downloadSurveyCSV(forYear: 2022)
|
||||
let database = try Database(store: .inMemory)
|
||||
let connection = try database.connect()
|
||||
|
||||
// For our query, we'll count and group by the Country column in our CSV
|
||||
let result = try connection.query("""
|
||||
SELECT Country, COUNT(Country) AS Count
|
||||
FROM '\(fileURL.path)'
|
||||
GROUP BY Country
|
||||
ORDER BY Count DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
// We know our Country column has VARCHAR as its underlying data type and we
|
||||
// know BIGINT is the underlying data type for the Count column. Let's cast
|
||||
// them appropriately.
|
||||
let countryColumn = result[0].cast(to: String.self)
|
||||
let countColumn = result[1].cast(to: Int.self)
|
||||
|
||||
// Now that our columns have been cast, we can pull out the values. As
|
||||
// Column conforms to Collection, this is fairly simple. However, we do need
|
||||
// to guard against nil values and replace with a suitable default
|
||||
let countries = countryColumn.map { $0 ?? "unknown" }
|
||||
let counts = countColumn.map { $0 ?? 0 }
|
||||
|
||||
// Finally, we can zip up the results and generate our chart rows
|
||||
var rows = [ChartRow]()
|
||||
for (country, count) in zip(countries, counts) {
|
||||
let row = ChartRow(nameOfCountry: country, participantCount: count)
|
||||
rows.append(row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
/*:
|
||||
|
||||
## Step 2: Making Charts
|
||||
|
||||
Now that we have our data extraction routine ready to go, we can look into
|
||||
visualizing it into something that's great to look at and easy to understand.
|
||||
Thankfully, the combination of DuckDB, SwiftUI and Swift Charts make this
|
||||
process really easy.
|
||||
|
||||
As in the previous playground we'll create a SwiftUI view to display our chart
|
||||
that displays a spinner while the data is assembled using our routine from step 1.
|
||||
Then, when our data is ready, we'll update our view and use Swift Charts to
|
||||
visualize what we've gathered.
|
||||
|
||||
Let's take a look.
|
||||
*/
|
||||
|
||||
struct PopularCountriesView: View {
|
||||
|
||||
enum ViewState {
|
||||
case loading
|
||||
case data([ChartRow])
|
||||
}
|
||||
|
||||
@State private var state = ViewState.loading
|
||||
|
||||
var body: some View {
|
||||
switch state {
|
||||
|
||||
// If we're still loading, we'll display a spinner and kick-off our
|
||||
// routine from step 1
|
||||
case .loading:
|
||||
ProgressView { Text("Fetching Data") }
|
||||
.task {
|
||||
do {
|
||||
let chartData = try await generateSurveyParticipantCountryChartData()
|
||||
self.state = .data(chartData)
|
||||
}
|
||||
catch { print("unexpected error: \(error)") }
|
||||
}
|
||||
|
||||
// Now we have our chart data we can display the chart
|
||||
case .data(let chartRows):
|
||||
Chart(chartRows) { row in
|
||||
BarMark(
|
||||
x: .value("Respondents", row.participantCount),
|
||||
y: .value("Country", row.nameOfCountry)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*:
|
||||
|
||||
## Conclusion
|
||||
|
||||
In just a few lines of code DuckDB has enabled you to:
|
||||
|
||||
- Open a CSV file withing DuckDB
|
||||
- Analyze it using SQL
|
||||
- Prepare it for display using Swift Charts.
|
||||
|
||||
You've also learnt how you to cast from DuckDB's underlying database types and
|
||||
into native Swift types using Column's `cast(to:)` methods.
|
||||
|
||||
Now you're well on your way to becoming a DuckDB master!
|
||||
*/
|
||||
|
||||
PlaygroundPage.current.setLiveView(
|
||||
PopularCountriesView()
|
||||
.frame(width: 640, height: 480, alignment: .center)
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Timeline
|
||||
version = "3.0">
|
||||
<TimelineItems>
|
||||
</TimelineItems>
|
||||
</Timeline>
|
||||
65
external/duckdb/tools/swift/duckdb-swift/DuckDBPlayground.playground/Sources/SurveyLoader.swift
vendored
Normal file
65
external/duckdb/tools/swift/duckdb-swift/DuckDBPlayground.playground/Sources/SurveyLoader.swift
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
import Foundation
|
||||
import PlaygroundSupport
|
||||
import System
|
||||
|
||||
public enum SurveyLoader {
|
||||
|
||||
enum Failure {
|
||||
case unableToDownloadSurveyContent
|
||||
}
|
||||
|
||||
private static let localDataURL = playgroundSharedDataDirectory
|
||||
.appending(component: "org.duckdb", directoryHint: .isDirectory)
|
||||
.appending(component: "surveys")
|
||||
|
||||
public static func downloadSurveyCSV(forYear year: Int) async throws -> URL {
|
||||
try await downloadSurveyIfNeeded(forYear: year)
|
||||
return localSurveyDataURL(forYear: year)
|
||||
.appending(component: "survey_results_public.csv")
|
||||
}
|
||||
|
||||
public static func downloadSurveySchemaCSV(forYear year: Int) async throws -> URL {
|
||||
try await downloadSurveyIfNeeded(forYear: year)
|
||||
return localSurveyDataURL(forYear: year)
|
||||
.appending(component: "survey_results_schema.csv")
|
||||
}
|
||||
|
||||
public static func downloadSurveyIfNeeded(forYear year: Int) async throws {
|
||||
let surveyDataURL = localSurveyDataURL(forYear: year)
|
||||
if FileManager.default.fileExists(atPath: surveyDataURL.path) {
|
||||
// survey already exists. skipping.
|
||||
return
|
||||
}
|
||||
let surveyURL = remoteSurveyDataZipURL(forYear: year)
|
||||
let (zipFileURL, _) = try await URLSession.shared.download(from: surveyURL)
|
||||
try FileManager.default.createDirectory(
|
||||
at: surveyDataURL, withIntermediateDirectories: true)
|
||||
try Shell.execute("unzip '\(zipFileURL.path)' -d '\(surveyDataURL.path)'")
|
||||
}
|
||||
|
||||
private static func localSurveyDataURL(forYear year: Int) -> URL {
|
||||
localDataURL.appending(component: "\(year)")
|
||||
}
|
||||
|
||||
private static func remoteSurveyDataZipURL(forYear year: Int) -> URL {
|
||||
URL(string: "https://info.stackoverflowsolutions.com/rs/719-EMH-566/images/stack-overflow-developer-survey-\(year).zip")!
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum Shell {
|
||||
|
||||
@discardableResult
|
||||
static func execute(_ command: String) throws -> String {
|
||||
let process = Process()
|
||||
let pipe = Pipe()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||
process.arguments = ["-c", command]
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
return String(data: data, encoding: .utf8)!
|
||||
}
|
||||
}
|
||||
8
external/duckdb/tools/swift/duckdb-swift/DuckDBPlayground.playground/contents.xcplayground
vendored
Normal file
8
external/duckdb/tools/swift/duckdb-swift/DuckDBPlayground.playground/contents.xcplayground
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='macos' display-mode='rendered' buildActiveScheme='true' importAppTypes='true'>
|
||||
<pages>
|
||||
<page name='1. Welcome'/>
|
||||
<page name='2. Getting Started'/>
|
||||
<page name='3. Visualizing Data'/>
|
||||
</pages>
|
||||
</playground>
|
||||
7
external/duckdb/tools/swift/duckdb-swift/DuckDBPlayground.playground/playground.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
7
external/duckdb/tools/swift/duckdb-swift/DuckDBPlayground.playground/playground.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
Reference in New Issue
Block a user