should be it

This commit is contained in:
2025-10-24 19:21:19 -05:00
parent a4b23fc57c
commit f09560c7b1
14047 changed files with 3161551 additions and 1 deletions

View File

@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FILEHEADER</key>
<string>
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-___YEAR___ Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.</string>
</dict>
</plist>

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DuckDB_DuckDBTests"
BuildableName = "DuckDB_DuckDBTests"
BlueprintName = "DuckDB_DuckDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DuckDB"
BuildableName = "DuckDB"
BlueprintName = "DuckDB"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DuckDBTests"
BuildableName = "DuckDBTests"
BlueprintName = "DuckDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Cduckdb"
BuildableName = "Cduckdb"
BlueprintName = "Cduckdb"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DuckDBTests"
BuildableName = "DuckDBTests"
BlueprintName = "DuckDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DuckDB_DuckDBTests"
BuildableName = "DuckDB_DuckDBTests"
BlueprintName = "DuckDB_DuckDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FILEHEADER</key>
<string>
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-___YEAR___ Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.</string>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -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)
*/

View File

@@ -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)
)

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Timeline
version = "3.0">
<TimelineItems>
</TimelineItems>
</Timeline>

View File

@@ -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)
)

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Timeline
version = "3.0">
<TimelineItems>
</TimelineItems>
</Timeline>

View 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)!
}
}

View 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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,362 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
6A1C890A29C3991E00A7DED1 /* ExplorerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1C890929C3991E00A7DED1 /* ExplorerApp.swift */; };
6A1C890C29C3991E00A7DED1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1C890B29C3991E00A7DED1 /* ContentView.swift */; };
6A1C890E29C3991F00A7DED1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A1C890D29C3991F00A7DED1 /* Assets.xcassets */; };
6A1C891A29C39BEB00A7DED1 /* DuckDB in Frameworks */ = {isa = PBXBuildFile; productRef = 6A1C891929C39BEB00A7DED1 /* DuckDB */; };
6A72088829C4B5AA00061043 /* ExoplanetStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A72088729C4B5AA00061043 /* ExoplanetStore.swift */; };
6AAC2F5E29C4A37400ADC955 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AAC2F5D29C4A37400ADC955 /* ErrorView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
6A1C890629C3991E00A7DED1 /* ExoplanetExplorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExoplanetExplorer.app; sourceTree = BUILT_PRODUCTS_DIR; };
6A1C890929C3991E00A7DED1 /* ExplorerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerApp.swift; sourceTree = "<group>"; };
6A1C890B29C3991E00A7DED1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
6A1C890D29C3991F00A7DED1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6A1C891729C39BA500A7DED1 /* duckdb-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "duckdb-swift"; path = ../..; sourceTree = "<group>"; };
6A72088729C4B5AA00061043 /* ExoplanetStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExoplanetStore.swift; sourceTree = "<group>"; };
6AAC2F5D29C4A37400ADC955 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
6A1C890329C3991E00A7DED1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6A1C891A29C39BEB00A7DED1 /* DuckDB in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
6A1C88FD29C3991E00A7DED1 = {
isa = PBXGroup;
children = (
6A1C891729C39BA500A7DED1 /* duckdb-swift */,
6A1C890829C3991E00A7DED1 /* ExoplanetExplorer */,
6A1C890729C3991E00A7DED1 /* Products */,
6A1C891829C39BEB00A7DED1 /* Frameworks */,
);
sourceTree = "<group>";
};
6A1C890729C3991E00A7DED1 /* Products */ = {
isa = PBXGroup;
children = (
6A1C890629C3991E00A7DED1 /* ExoplanetExplorer.app */,
);
name = Products;
sourceTree = "<group>";
};
6A1C890829C3991E00A7DED1 /* ExoplanetExplorer */ = {
isa = PBXGroup;
children = (
6A1C890B29C3991E00A7DED1 /* ContentView.swift */,
6AAC2F5D29C4A37400ADC955 /* ErrorView.swift */,
6A72088729C4B5AA00061043 /* ExoplanetStore.swift */,
6A1C890929C3991E00A7DED1 /* ExplorerApp.swift */,
6A1C890D29C3991F00A7DED1 /* Assets.xcassets */,
);
path = ExoplanetExplorer;
sourceTree = "<group>";
};
6A1C891829C39BEB00A7DED1 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
6A1C890529C3991E00A7DED1 /* ExoplanetExplorer */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6A1C891429C3991F00A7DED1 /* Build configuration list for PBXNativeTarget "ExoplanetExplorer" */;
buildPhases = (
6A1C890229C3991E00A7DED1 /* Sources */,
6A1C890329C3991E00A7DED1 /* Frameworks */,
6A1C890429C3991E00A7DED1 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ExoplanetExplorer;
packageProductDependencies = (
6A1C891929C39BEB00A7DED1 /* DuckDB */,
);
productName = ExoPlanetExplorer;
productReference = 6A1C890629C3991E00A7DED1 /* ExoplanetExplorer.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
6A1C88FE29C3991E00A7DED1 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1420;
TargetAttributes = {
6A1C890529C3991E00A7DED1 = {
CreatedOnToolsVersion = 14.2;
};
};
};
buildConfigurationList = 6A1C890129C3991E00A7DED1 /* Build configuration list for PBXProject "ExoplanetExplorer" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 6A1C88FD29C3991E00A7DED1;
productRefGroup = 6A1C890729C3991E00A7DED1 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
6A1C890529C3991E00A7DED1 /* ExoplanetExplorer */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
6A1C890429C3991E00A7DED1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6A1C890E29C3991F00A7DED1 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
6A1C890229C3991E00A7DED1 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6A1C890C29C3991E00A7DED1 /* ContentView.swift in Sources */,
6A1C890A29C3991E00A7DED1 /* ExplorerApp.swift in Sources */,
6AAC2F5E29C4A37400ADC955 /* ErrorView.swift in Sources */,
6A72088829C4B5AA00061043 /* ExoplanetStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
6A1C891229C3991F00A7DED1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
6A1C891329C3991F00A7DED1 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
6A1C891529C3991F00A7DED1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.duckdb.ExoPlanetExplorer;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
6A1C891629C3991F00A7DED1 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.duckdb.ExoPlanetExplorer;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
6A1C890129C3991E00A7DED1 /* Build configuration list for PBXProject "ExoplanetExplorer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6A1C891229C3991F00A7DED1 /* Debug */,
6A1C891329C3991F00A7DED1 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
6A1C891429C3991F00A7DED1 /* Build configuration list for PBXNativeTarget "ExoplanetExplorer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6A1C891529C3991F00A7DED1 /* Debug */,
6A1C891629C3991F00A7DED1 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
6A1C891929C39BEB00A7DED1 /* DuckDB */ = {
isa = XCSwiftPackageProductDependency;
productName = DuckDB;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 6A1C88FE29C3991E00A7DED1 /* Project object */;
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6A1C890529C3991E00A7DED1"
BuildableName = "ExoplanetExplorer.app"
BlueprintName = "ExoplanetExplorer"
ReferencedContainer = "container:ExoplanetExplorer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6A1C890529C3991E00A7DED1"
BuildableName = "ExoplanetExplorer.app"
BlueprintName = "ExoplanetExplorer"
ReferencedContainer = "container:ExoplanetExplorer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6A1C890529C3991E00A7DED1"
BuildableName = "ExoplanetExplorer.app"
BlueprintName = "ExoplanetExplorer"
ReferencedContainer = "container:ExoplanetExplorer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,102 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Charts
import SwiftUI
@preconcurrency import TabularData
struct ContentView: View {
private enum ViewState {
case fetching(Error?)
case loaded(DataFrame)
}
let exoplanetStore: ExoplanetStore
@State private var state = ViewState.fetching(nil)
var body: some View {
Group {
switch state {
case .loaded(let dataFrame):
VStack {
Text("Number of Exoplanets Discovered By Year")
.font(.title)
.fontDesign(.default)
.fontWeight(.bold)
.multilineTextAlignment(.center)
DiscoveryYearChart(dataFrame: dataFrame)
}
case .fetching(nil):
ProgressView { Text("Fetching Data") }
case .fetching(let error?):
ErrorView(title: "Query Failed", error: error)
}
}
.padding()
.task {
do {
let frame = try await exoplanetStore.groupedByDiscoveryYear()
self.state = .loaded(frame)
}
catch {
self.state = .fetching(error)
}
}
}
}
// MARK: - Chart View
struct DiscoveryYearChart: View {
private struct ChartRow {
let year: Date
let count: Int
}
let dataFrame: DataFrame
private var rows: [ChartRow] {
let yearColumn = dataFrame.columns[0].assumingType(Int.self).filled(with: 9999)
let countColumn = dataFrame.columns[1].assumingType(Int.self).filled(with: -1)
let calendar = Calendar(identifier: .gregorian)
var rows = [ChartRow]()
for (year, count) in zip(yearColumn, countColumn) {
let dateComponents = DateComponents(calendar: calendar, year: year)
let date = dateComponents.date ?? .distantPast
rows.append(ChartRow(year: date, count: count))
}
return rows
}
var body: some View {
Chart(rows, id: \.year) { row in
BarMark(
x: .value("Year", row.year, unit: .year),
y: .value("Count", row.count)
)
}
}
}

View File

@@ -0,0 +1,58 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import SwiftUI
struct ErrorView: View {
let title: String
let error: Error
let retryAction: (() -> Void)?
init(title: String, error: Error, retryAction: (() -> Void)? = nil) {
self.title = title
self.error = error
self.retryAction = retryAction
}
var body: some View {
VStack(spacing: 8) {
Text("☠️")
.font(.largeTitle)
Text(title)
.font(.subheadline)
.foregroundColor(.gray)
.fontWeight(.bold)
Text(error.localizedDescription)
.font(.caption)
.foregroundColor(.gray)
if let retryAction {
Button("Retry") { retryAction() }
.buttonStyle(.borderedProminent)
.padding()
}
}
.multilineTextAlignment(.center)
}
}

View File

@@ -0,0 +1,91 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import DuckDB
import Foundation
import TabularData
final class ExoplanetStore {
static func create() async throws -> ExoplanetStore {
let (csvFileURL, _) = try await URLSession.shared.download(from: Self.csvRemoteURL)
let database = try Database(store: .inMemory)
let connection = try database.connect()
let _ = try connection.query(
"CREATE TABLE exoplanets AS SELECT * FROM read_csv_auto('\(csvFileURL.path)');")
return ExoplanetStore(database: database, connection: connection)
}
let database: Database
let connection: Connection
private init(database: Database, connection: Connection) {
self.database = database
self.connection = connection
}
func groupedByDiscoveryYear() async throws -> DataFrame {
let result = try connection.query("""
SELECT disc_year, COUNT(disc_year) AS Count
FROM exoplanets
GROUP BY disc_year
ORDER BY disc_year
""")
let discoveryYearColumn = result[0].cast(to: Int.self)
let countColumn = result[1].cast(to: Int.self)
return DataFrame(columns: [
TabularData.Column(discoveryYearColumn)
.eraseToAnyColumn(),
TabularData.Column(countColumn)
.eraseToAnyColumn(),
])
}
}
private extension ExoplanetStore {
static let csvRemoteURL: URL = {
let apiEndpointURL = URL(
string: "https://exoplanetarchive.ipac.caltech.edu/TAP/sync")!
// column descriptions available at:
// https://exoplanetarchive.ipac.caltech.edu/docs/API_PS_columns.html
let remoteQueryColumns = [
"pl_name",
"hostname",
"sy_snum",
"disc_year",
"disc_facility",
"st_mass",
"st_rad",
"st_age",
]
let remoteQuery = """
SELECT+\(remoteQueryColumns.joined(separator: "+,+"))+FROM+pscomppars
"""
return apiEndpointURL.appending(queryItems: [
.init(name: "query", value: remoteQuery),
.init(name: "format", value: "csv"),
])
}()
}

View File

@@ -0,0 +1,67 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import SwiftUI
@main
struct ExplorerApp: App {
private enum ViewState {
case loading(Error?)
case ready(ExoplanetStore)
}
@State private var state = ViewState.loading(nil)
var body: some Scene {
WindowGroup {
Group {
switch state {
case .ready(let exoplanetStore):
ContentView(exoplanetStore: exoplanetStore)
case .loading(nil):
ProgressView { Text("Loading Exoplanets") }
case .loading(let error?):
ErrorView(title: "Failed To Load Exoplanets", error: error) {
Task { await prepareExoplanetsStore() }
}
}
}
.task {
await prepareExoplanetsStore()
}
}
}
private func prepareExoplanetsStore() async {
guard case .loading(_) = state else { return }
self.state = .loading(nil)
do {
self.state = .ready(try await ExoplanetStore.create())
}
catch {
self.state = .loading(error)
}
}
}

View File

@@ -0,0 +1,7 @@
Copyright 2018-2024 Stichting DuckDB Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,68 @@
<div align="center">
<img src="https://duckdb.org/images/logo-dl/DuckDB_Logo-stacked.svg" height="120">
</div>
<br>
<p align="center">
<a href="https://github.com/duckdb/duckdb/actions">
<img src="https://github.com/duckdb/duckdb/actions/workflows/Main.yml/badge.svg?branch=main" alt="Github Actions Badge">
</a>
<a href="https://app.codecov.io/gh/duckdb/duckdb">
<img src="https://codecov.io/gh/duckdb/duckdb/branch/main/graph/badge.svg?token=FaxjcfFghN" alt="codecov"/>
</a>
<a href="https://discord.gg/tcvwpjfnZx">
<img src="https://shields.io/discord/909674491309850675" alt="discord" />
</a>
<a href="https://github.com/duckdb/duckdb/releases/">
<img src="https://img.shields.io/github/v/release/duckdb/duckdb?color=brightgreen&display_name=tag&logo=duckdb&logoColor=white" alt="Latest Release">
</a>
</p>
## DuckDB Swift
DuckDB Swift is the native Swift API for DuckDB. It employs a modern Swift
based API for clients across Apple, Linux and Windows platforms.
## DuckDB
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 on our website](https://duckdb.org/why_duckdb).
## Installation
To use DuckDB in your Swift based project:
1. Add DuckDB to your `Swift.package` dependencies:
```swift
.package(url: "https://github.com/duckdb/duckdb-swift", .upToNextMajor(from: .init(1, 0, 0))),
```
2. Add `DuckDB` as a dependency to your target:
```swift
.target(name: "TargetName", dependencies: [
.product(name: "DuckDB", package: "duckdb-swift"),
...
]),
```
## Documentation and Playgrounds
The DuckDB Swift API is fully documented using DocC and the documentation can be generated from within Xcode via `Product > Build Documentation`.
You can also explore an Xcode Playground that demonstrates getting up and running with DuckDB and visualizing data using SwiftUI. To access the playground, clone this repository, open the `DuckDB.xcworkspace` file, and navigate to the `DuckDBPlayground` from within Xcode's project explorer panel.
## Contributing
Issues and Pull Requests should be submitted via [the main DuckDB repository](https://github.com/duckdb/duckdb).
## Development
Development is managed through [the main DuckDB repository](https://github.com/duckdb/duckdb). To set-up a Swift development environment:
1. Check out the main DuckDB repository at [`https://github.com/duckdb/duckdb`](https://github.com/duckdb/duckdb)
2. Pull the latest git tags:
```shell
git fetch --all --tags
```
3. Generate the Unified Build files for the package:
```shell
python3 tools/swift/create_package.py tools/swift
```
4. Open the Xcode workspace at `tools/swift/duckdb-swift/DuckDB.xcworkspace`
Please also refer to our [Contribution Guide](https://github.com/duckdb/duckdb/blob/main/CONTRIBUTING.md).

View File

@@ -0,0 +1,432 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
/// An object that efficiently appends data to a DuckDB table
///
/// Appenders are the most efficient way of loading data into DuckDB from within
/// Swift, and are recommended for fast data loading. The appender is much
/// faster than using prepared statements or individual INSERT INTO statements.
///
/// Appends are made in row-wise format. For every column an `append(_:)` call
/// should be made after which the row should be finished by calling
/// ``endRow()``.
///
/// The following example shows two rows being appended to a table with a two
/// column layout.
///
/// ```swift
/// do {
/// let database = try Database(store: .inMemory)
/// let connection = try database.connect()
/// try connection.execute("CREATE TABLE people(id INTEGER, name VARCHAR)")
/// let appender = try Appender(connection: connection, table: "people")
/// // add first row
/// try appender.append(Int32(1))
/// try appender.append("Mark")
/// try appender.endRow()
/// // add second row
/// try appender.append(Int32(2))
/// try appender.append("Hannes")
/// try appender.endRow()
/// // flush rows to table
/// try appender.flush()
/// }
/// catch {
/// // handle error
/// }
/// ```
public final class Appender {
private let connection: Connection
private let ptr = UnsafeMutablePointer<duckdb_appender?>.allocate(capacity: 1)
/// Creates a new appender
///
/// Instantiates an ``Appender`` through which table rows can be efficiently
/// added
///
/// - Parameter connection: the connection through which rows should be
/// appended
/// - Parameter schema: the database schema (defaults to `main`)
/// - Parameter table: the table to append to
/// - Throws: ``DatabaseError/appenderFailedToInitialize(reason:)`` if the
/// appender failed to instantiate
public init(connection: Connection, schema: String? = nil, table: String) throws {
self.connection = connection
let status = schema.withOptionalCString { schemaStrPtr in
table.withCString { tableStrPtr in
connection.withCConnection { duckdb_appender_create($0, schemaStrPtr, tableStrPtr, ptr) }
}
}
guard .success == status else {
throw DatabaseError.appenderFailedToInitialize(reason: appenderError())
}
}
deinit {
try? flush()
duckdb_appender_destroy(ptr)
ptr.deallocate()
}
/// Signals the end of the current row
///
/// After all columns for a row have been appended, ``endRow()`` must be
/// called to indicate that the row is ready to be added to the database.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToEndRow(reason:)``
/// if the row could not be completed in its current state
public func endRow() throws {
let status = duckdb_appender_end_row(ptr.pointee)
guard .success == status else {
throw DatabaseError.appenderFailedToEndRow(reason: appenderError())
}
}
/// Flushes pending rows to the database
///
/// To optimize performance, the appender writes rows to the database in
/// batches. Use `flush()` to immediately write any rows that are pending
/// insertion.
///
/// - Throws: ``DatabaseError/appenderFailedToFlush(reason:)`` if the pending
/// rows failed to be written to the database
public func flush() throws {
let status = duckdb_appender_flush(ptr.pointee)
guard .success == status else {
throw DatabaseError.appenderFailedToFlush(reason: appenderError())
}
}
}
public extension Appender {
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Bool?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_bool(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Int8?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_int8(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Int16?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_int16(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Int32?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_int32(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Int64?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_int64(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: IntHuge?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_hugeint(ptr.pointee, .init(value)) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: UIntHuge?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_uhugeint(ptr.pointee, .init(value)) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: UInt8?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_uint8(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: UInt16?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_uint16(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: UInt32?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_uint32(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: UInt64?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_uint64(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Float?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_float(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Double?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_double(ptr.pointee, value) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Date?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_date(ptr.pointee, .init(date: value)) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Time?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand { duckdb_append_time(ptr.pointee, .init(time: value)) }
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Timestamp?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand {
duckdb_append_timestamp(ptr.pointee, .init(timestamp: value))
}
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Interval?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand {
duckdb_append_interval(ptr.pointee, .init(interval: value))
}
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: String?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
let data = value.data(using: .utf8)!
try withThrowingCommand {
data.withUnsafeBytes { dataPtr in
duckdb_append_varchar_length(ptr.pointee, dataPtr.baseAddress, .init(dataPtr.count))
}
}
}
/// Appends a value for the current row of the given type
///
/// Appends are made in row-wise format. For every column, an `append(_:)`
/// call should be made, after which the row should be finished by calling
/// ``endRow()``.
///
/// - Parameter value: the value to append
/// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
/// if a value of this type was not expected in the appender's current state
func append(_ value: Data?) throws {
guard let value = try unwrapValueOrAppendNull(value) else { return }
try withThrowingCommand {
value.withUnsafeBytes { dataPtr in
duckdb_append_blob( ptr.pointee, dataPtr.baseAddress, .init(dataPtr.count)) }
}
}
}
private extension Appender {
func appendNullValue() throws {
try withThrowingCommand { duckdb_append_null(ptr.pointee) }
}
func unwrapValueOrAppendNull<T>(_ value: T?) throws -> T? {
guard let value else {
try appendNullValue()
return nil
}
return value
}
func withThrowingCommand(_ body: () throws -> duckdb_state) throws {
let state = try body()
guard state == .success else {
throw DatabaseError.appenderFailedToAppendItem(reason: appenderError())
}
}
func appenderError() -> String? {
duckdb_appender_error(ptr.pointee).map(String.init(cString:))
}
}

View File

@@ -0,0 +1,52 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
public struct CodingUserInfoKeys {
/// This key is set on the `userInfo` dictionary of the `Decoder` that is
/// used when transforming data into a `Decodable`. The value is the ``LogicalType``
/// of the element being decoded. This can be used to implement dynamic decoding
/// behavior based on the underlying database type.
///
/// For example:
/// ```swift
/// struct DynamicDecodable: Decodable {
/// init(from decoder: Decoder) throws {
/// guard let logicalType = decoder.userInfo[CodingUserInfoKeys.logicalType] as? LogicalType else {
/// throw Error.expectedLogicalType
/// }
/// switch logicalType.dataType {
/// case .list:
/// let unkeyedContainer = try decoder.unkeyedContainer()
/// ...
/// case .map, .struct:
/// let keyedContainer = try decoder.container(keyedBy: AnyCodingKey.self)
/// ...
/// }
/// }
/// }
///
/// let column = result[0].cast(to: DynamicDecodable.self)
/// ```
public static let logicalTypeCodingUserInfoKey = CodingUserInfoKey(rawValue: "logicalType")!
}

View File

@@ -0,0 +1,466 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
/// A DuckDB result set column
///
/// DuckDB columns represent a vertical slice of a result set table. All DuckDB
/// columns have an underlying database type (accessed via the
/// ``underlyingDatabaseType`` member) which determine the native Swift types to
/// which the column can be cast to.
///
/// When columns are initially retrieved from a ``ResultSet`` through its
/// ``ResultSet/subscript(_:)`` accessor they have an element type of `Void`.
/// Only after a column is cast to a matching native type can its elements be
/// accessed.
///
/// For example, a column with an underlying database type of
/// ``DatabaseType/varchar`` can be cast to type `String`:
///
/// ```swift
/// // casts the first column in a result set to string
/// let column = result[0].cast(to: String.self)
/// ```
///
/// The documentation for each ``DatabaseType`` member specifies which native
/// Swift types a column may be cast to.
///
/// As a column is a Swift `Collection` type, once a column has been
/// successfully cast its elements can be accessed in the same way as any other
/// Swift collection type.
///
/// ```swift
/// for element in column {
/// print("element: \(element)")
/// }
/// ```
public struct Column<DataType> {
private let result: ResultSet
private let columnIndex: DBInt
private let unwrap: @Sendable (Vector.Element) throws -> DataType?
init(
result: ResultSet,
columnIndex: DBInt,
unwrap: @escaping @Sendable (Vector.Element) throws -> DataType?
) {
self.result = result
self.columnIndex = columnIndex
self.unwrap = unwrap
}
/// The name of the table column
public var name: String {
result.columnName(at: columnIndex)
}
/// The native Swift type to which the column has been cast
public var dataType: DataType.Type {
DataType.self
}
/// The underlying primitive database type of the column
public var underlyingDatabaseType: DatabaseType {
result.columnDataType(at: columnIndex)
}
/// The underlying logical type of the column
public var underlyingLogicalType: LogicalType {
result.columnLogicalType(at: columnIndex)
}
}
// MARK: - Type Casting
public extension Column {
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Void.Type) -> Column<Void> {
.init(result: result, columnIndex: columnIndex) { $0.unwrapNull() ? nil : () }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Bool.Type) -> Column<Bool> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Bool.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Warning: Implicit conversion of a DuckDB integer column value greater
/// than `Int.max` or less than `Int.min` is a programmer error and will
/// result in a runtime precondition failure
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Int.Type) -> Column<Int> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Int.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Int8.Type) -> Column<Int8> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Int8.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Int16.Type) -> Column<Int16> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Int16.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Int32.Type) -> Column<Int32> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Int32.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Int64.Type) -> Column<Int64> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Int64.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: IntHuge.Type) -> Column<IntHuge> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(IntHuge.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UIntHuge.Type) -> Column<UIntHuge> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UIntHuge.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Warning: Implicit conversion of a DuckDB integer column value greater
/// than `UInt.max` is a programmer error and will result in a runtime
/// precondition failure
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UInt.Type) -> Column<UInt> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UInt.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UInt8.Type) -> Column<UInt8> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UInt8.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UInt16.Type) -> Column<UInt16> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UInt16.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UInt32.Type) -> Column<UInt32> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UInt32.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UInt64.Type) -> Column<UInt64> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UInt64.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Float.Type) -> Column<Float> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Float.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Double.Type) -> Column<Double> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Double.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: String.Type) -> Column<String> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(String.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: UUID.Type) -> Column<UUID> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(UUID.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Time.Type) -> Column<Time> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Time.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between the
/// given type and the column's underlying database type, returned elements
/// will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: TimeTz.Type) -> Column<TimeTz> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(TimeTz.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Date.Type) -> Column<Date> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Date.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Timestamp.Type) -> Column<Timestamp> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Timestamp.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Interval.Type) -> Column<Interval> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Interval.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Data.Type) -> Column<Data> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Data.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast(to type: Decimal.Type) -> Column<Decimal> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrap(Decimal.self) }
}
/// Casts the column to the given type
///
/// A column cast always succeeds but if there is a type-mismatch between
/// the given type and the column's underlying database type, returned
/// elements will always be equal to `nil`.
///
/// - Parameter type: the native Swift type to cast to
/// - Returns: a typed DuckDB result set ``Column``
func cast<T: Decodable>(to type: T.Type) -> Column<T> {
.init(result: result, columnIndex: columnIndex) { try $0.unwrapDecodable(T.self) }
}
}
// MARK: - Collection conformance
extension Column: RandomAccessCollection {
public typealias Element = DataType?
public struct Iterator: IteratorProtocol {
private let column: Column
private var position: DBInt
init(column: Column) {
self.column = column
self.position = column.startIndex
}
public mutating func next() -> Element? {
guard position < column.endIndex else { return nil }
defer { position += 1 }
return .some(column[position])
}
}
public subscript(position: DBInt) -> DataType? {
try? unwrap(result.element(forColumn: columnIndex, at: position))
}
public var startIndex: DBInt { 0 }
public var endIndex: DBInt { result.rowCount }
public func makeIterator() -> Iterator {
Iterator(column: self)
}
public func index(after i: DBInt) -> DBInt { i + 1 }
public func index(before i: DBInt) -> DBInt { i - 1 }
}
// MARK: - Sendable conformance
extension Column: Sendable where DataType: Sendable {}
// MARK: - Identifiable conformance
extension Column: Identifiable {
public var id: String { name }
}

View File

@@ -0,0 +1,133 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
public extension Database {
/// An object representing a DuckDB database configuration
///
/// Configuration options can be provided to change different settings of the
/// database system. Note that many of these settings can be changed later on
/// using PRAGMA statements as well. The configuration object should be
/// created, filled with values using ``setValue(_:forKey:)`` and passed to
/// ``Database/init(store:configuration:)``
///
/// The following example sets up an in-memory database with some
/// configuration options set
///
/// ```swift
/// do {
/// let configuration = Configuration()
/// configuration.setValue("READ_WRITE", forKey: "access_mode")
/// configuration.setValue("8", forKey: "threads")
/// configuration.setValue("8GB", forKey: "max_memory")
/// configuration.setValue("DESC", forKey: "default_order")
/// let database = try Database(store: .inMemory, configuration: configuration)
/// let connection = try database.connect()
/// }
/// catch {
/// // handle error
/// }
/// ```
/// Use ``Database/Configuration/options`` to see the full list of available
/// configuration options.
final class Configuration {
private let ptr = UnsafeMutablePointer<duckdb_config?>.allocate(capacity: 1)
/// Creates a DuckDB configuration
public init() {
let status = duckdb_create_config(ptr)
guard status == .success else { fatalError("malloc failure") }
}
deinit {
duckdb_destroy_config(ptr)
ptr.deallocate()
}
func withCConfiguration<T>(_ body: (duckdb_config?) throws -> T) rethrows -> T {
try body(ptr.pointee)
}
/// Set a value on the configuration object
///
/// Applies a configuration option to the configuration object. Use
/// ``Database/Configuration/options`` to see the full list of available
/// configuration options.
///
/// - Parameter key: the name of the option being set
/// - Parameter value: the desired value of the option being set
/// - Throws: ``DatabaseError/configurationFailedToSetFlag`` if the option
/// is not available or the value is malformed
public func setValue(_ value: String, forKey key: String) throws {
try value.withCString { valueCStr in
try key.withCString { keyCStr in
let status = duckdb_set_config(ptr.pointee, keyCStr, valueCStr)
guard .success == status else {
throw DatabaseError.configurationFailedToSetFlag
}
}
}
}
}
}
// MARK: - Option
public extension Database.Configuration {
/// A DuckDB database configuration option information type
struct OptionInfo {
/// The name/key of the option
public let name: String
/// An overview of the option and its potential values
public let description: String
}
/// A list of all available database configuration options with their
/// descriptions
static var options: [OptionInfo] {
let count = duckdb_config_count()
var options = [OptionInfo]()
let outName = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(capacity: 1)
let outDesc = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(capacity: 1)
defer {
outName.deallocate()
outDesc.deallocate()
}
for i in 0..<count {
duckdb_get_config_flag(i, outName, outDesc)
if let nameCStr = outName.pointee, let descCStr = outDesc.pointee {
let name = String(cString: nameCStr)
let desc = String(cString: descCStr)
options.append(OptionInfo(name: name, description: desc))
}
outName.pointee = nil
outDesc.pointee = nil
}
return options
}
}

View File

@@ -0,0 +1,106 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
/// An object representing a connection to a DuckDB database
///
/// A connection through which a database can be queried
///
/// For each database, you can create one or many connections using
/// ``Database/connect()``.
/// As individual connections are locked during querying it is recommended that
/// in contexts where blocking is undesirable, connections should be accessed
/// asynchronously through an actor or via a background queue.
///
/// The following example creates a new in-memory database and connects to it.
///
/// ```swift
/// do {
/// let database = try Database(store: .inMemory)
/// let connection = try database.connect()
/// }
/// catch {
/// // handle error
/// }
/// ```
public final class Connection: Sendable {
private let database: Database
private let ptr = UnsafeMutablePointer<duckdb_connection?>.allocate(capacity: 1)
/// Creates a new connection
///
/// Instantiates a connection through which a database can be queried
///
/// - Parameter database: the database to connect to
/// - Throws: ``DatabaseError/connectionFailedToInitialize`` if the connection
/// failed to instantiate
public init(database: Database) throws {
self.database = database
let status = database.withCDatabase { duckdb_connect($0, ptr) }
guard status == .success else { throw DatabaseError.connectionFailedToInitialize }
}
deinit {
duckdb_disconnect(ptr)
ptr.deallocate()
}
/// Perform a database query
///
/// Takes a raw SQL statement and passes it to the database engine for
/// execution, returning the result.
///
/// - Note: This is a blocking operation
/// - Parameter sql: the query string as SQL
/// - Throws: ``DatabaseError/connectionQueryError(reason:)`` if the query
/// did not execute successfully
/// - Returns: a query result set
public func query(_ sql: String) throws -> ResultSet {
try ResultSet(connection: self, sql: sql)
}
/// Execute a database query
///
/// Takes a raw SQL statement and passes it to the database engine for
/// execution, ignoring the result.
///
/// - Note: This is a blocking operation
/// - Parameter sql: the query string as SQL
/// - Throws: ``DatabaseError/connectionQueryError(reason:)`` if the query
/// did not execute successfully
public func execute(_ sql: String) throws {
let status = sql.withCString { queryStrPtr in
duckdb_query(ptr.pointee, queryStrPtr, nil)
}
guard status == .success else {
throw DatabaseError.connectionQueryError(reason: nil)
}
}
func withCConnection<T>(_ body: (duckdb_connection?) throws -> T) rethrows -> T {
try body(ptr.pointee)
}
}

View File

@@ -0,0 +1,135 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
/// DuckDB index type
public typealias DBInt = UInt64
/// An object representing a DuckDB database
///
/// To use DuckDB, you must first initialize a DuckDB ``Database``.
/// ``Database/init(store:configuration:)`` takes as parameter the database
/// store type. The ``Database/Store/inMemory`` option can be used to create an
/// in-memory database. Note that for an in-memory database no data is persisted
/// to disk (i.e. all data is lost when you exit the process).
///
/// With the ``Database`` instantiated, you can create one or many DuckDB
/// ``Connection`` instances using ``Database/connect()``. As individual
/// connections are locked during querying it is recommended that in contexts
/// where blocking is undesirable, connections should be accessed
/// asynchronously through an actor or via a background queue.
///
/// The following example creates a new in-memory database and connects to it.
///
/// ```swift
/// do {
/// let database = try Database(store: .inMemory)
/// let connection = try database.connect()
/// }
/// catch {
/// // handle error
/// }
/// ```
public final class Database: Sendable {
/// Duck DB database store type
public enum Store {
/// A local file based database store
case file(at: URL)
/// An in-memory database store
case inMemory
}
private let ptr = UnsafeMutablePointer<duckdb_database?>.allocate(capacity: 1)
/// Creates a Duck DB database
///
/// A DuckDB database can be initilaized using either a local database file or
/// using an in-memory store
///
/// - Note: An in-memory database does not persist data to disk. All data is
/// lost when you exit the process.
/// - Parameter store: the store to initialize the database with
/// - Parameter configuration: the configuration to initialize the database
/// with
/// - Throws: ``DatabaseError/databaseFailedToInitialize(reason:)`` if the
/// database failed to instantiate
public convenience init(
store: Store = .inMemory, configuration: Configuration? = nil
) throws {
var fileURL: URL?
if case .file(let url) = store {
guard url.isFileURL else {
throw DatabaseError.databaseFailedToInitialize(
reason: "provided URL for database store file must be local")
}
fileURL = url
}
try self.init(path: fileURL?.path, config: configuration)
}
private init(path: String?, config: Configuration?) throws {
let outError = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
defer { outError.deallocate() }
let status = path.withOptionalCString { strPtr in
if let config {
return config.withCConfiguration { cconfig in
duckdb_open_ext(strPtr, ptr, cconfig, outError)
}
}
else {
return duckdb_open_ext(strPtr, ptr, nil, outError)
}
}
guard status == .success else {
let error = outError.pointee.map { ptr in
defer { duckdb_free(ptr) }
return String(cString: ptr)
}
throw DatabaseError.databaseFailedToInitialize(reason: error)
}
}
deinit {
duckdb_close(ptr)
ptr.deallocate()
}
/// Creates a connection to the database
///
/// See ``Connection/init(database:)`` for further discussion
///
/// - Throws: ``DatabaseError/connectionFailedToInitialize`` if the connection
/// could not be instantiated
/// - Returns: a database connection
public func connect() throws -> Connection {
try .init(database: self)
}
func withCDatabase<T>(_ body: (duckdb_database?) throws -> T) rethrows -> T {
try body(ptr.pointee)
}
}

View File

@@ -0,0 +1,55 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
/// A DuckDB database error
public enum DatabaseError: Error {
/// Provided decimal overflows the internal database representaion
case decimalUnrepresentable
/// Supplied item could not be appended
case appenderFailedToAppendItem(reason: String?)
/// Row could not be ended with the appender in its current state
case appenderFailedToEndRow(reason: String?)
/// Appender could not be flushed in its current state
case appenderFailedToFlush(reason: String?)
/// Failed to instantiate appender
case appenderFailedToInitialize(reason: String?)
/// Failed to instantiate connection
case connectionFailedToInitialize
/// Failed to set flag on database configuration
case configurationFailedToSetFlag
/// Failed to execute query on connection
case connectionQueryError(reason: String?)
/// Failed to instantiate database
case databaseFailedToInitialize(reason: String?)
/// Failed to instantiate prepared statement
case preparedStatementFailedToInitialize(reason: String?)
/// Failed to bound value to prepared statement
case preparedStatementFailedToBindParameter(reason: String?)
/// Failed to execute prepared statement query
case preparedStatementQueryError(reason: String?)
/// Value of type could not be found
case valueNotFound(Any.Type)
/// Type does not match underlying database type
case typeMismatch(Any.Type)
}

View File

@@ -0,0 +1,158 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
/// The underlying database type of a DuckDB column
///
/// DuckDB is a strongly typed database system. As such, every column has a
/// single type specified. This type is constant over the entire column. That is
/// to say, a column with an underlying type of ``DatabaseType/integer`` will
/// only contain ``DatabaseType/integer`` values.
///
/// DuckDB also supports columns of composite types. For example, it is possible
/// to define an array of integers (`INT[]`), which can be cast to `[Int32]`
/// within Swift using ``Column/cast(to:)-4376d``. It is also possible to
/// define database types as arbitrary structs (`ROW(i INTEGER, j VARCHAR)`),
/// which can be cast to their `Decodable` matching Swift type in the same way.
public struct DatabaseType: RawRepresentable, Hashable, Equatable {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
}
// MARK: - Public Types
public extension DatabaseType {
/// Boolean type castable to `Bool`
static let boolean = DatabaseType(rawValue: DUCKDB_TYPE_BOOLEAN.rawValue)
/// Integer type castable to `Int8`
static let tinyint = DatabaseType(rawValue: DUCKDB_TYPE_TINYINT.rawValue)
/// Integer type castable to `Int16`
static let smallint = DatabaseType(rawValue: DUCKDB_TYPE_SMALLINT.rawValue)
/// Integer type castable to `Int32`
static let integer = DatabaseType(rawValue: DUCKDB_TYPE_INTEGER.rawValue)
/// Integer type castable to `Int64`
static let bigint = DatabaseType(rawValue: DUCKDB_TYPE_BIGINT.rawValue)
/// Integer type castable to `HugeInt`
static let hugeint = DatabaseType(rawValue: DUCKDB_TYPE_HUGEINT.rawValue)
/// Integer type castable to `UHugeInt`
static let uhugeint = DatabaseType(rawValue: DUCKDB_TYPE_UHUGEINT.rawValue)
/// Unsigned integer type castable to `UInt8`
static let utinyint = DatabaseType(rawValue: DUCKDB_TYPE_UTINYINT.rawValue)
/// Unsigned integer type castable to `UInt16`
static let usmallint = DatabaseType(rawValue: DUCKDB_TYPE_USMALLINT.rawValue)
/// Unsigned integer type castable to `UInt32`
static let uinteger = DatabaseType(rawValue: DUCKDB_TYPE_UINTEGER.rawValue)
/// Unsigned integer type castable to `UInt64`
static let ubigint = DatabaseType(rawValue: DUCKDB_TYPE_UBIGINT.rawValue)
/// Floating point type castable to `Float`
static let float = DatabaseType(rawValue: DUCKDB_TYPE_FLOAT.rawValue)
/// Floating point type castable to `Double`
static let double = DatabaseType(rawValue: DUCKDB_TYPE_DOUBLE.rawValue)
/// Timestamp type castable to `Timestamp`
static let timestamp = DatabaseType(rawValue: DUCKDB_TYPE_TIMESTAMP.rawValue)
/// Timestamp(TZ) type castable to `Timestamp`
static let timestampTz = DatabaseType(rawValue: DUCKDB_TYPE_TIMESTAMP_TZ.rawValue)
/// Date type castable to `Date`
static let date = DatabaseType(rawValue: DUCKDB_TYPE_DATE.rawValue)
/// Time type castable to `Time`
static let time = DatabaseType(rawValue: DUCKDB_TYPE_TIME.rawValue)
/// Time(TZ) type castable to `Time`
static let timeTz = DatabaseType(rawValue: DUCKDB_TYPE_TIME_TZ.rawValue)
/// Interval type castable to `Interval`
static let interval = DatabaseType(rawValue: DUCKDB_TYPE_INTERVAL.rawValue)
/// String type castable to `String`
static let varchar = DatabaseType(rawValue: DUCKDB_TYPE_VARCHAR.rawValue)
/// Data type castable to `Data`
static let blob = DatabaseType(rawValue: DUCKDB_TYPE_BLOB.rawValue)
/// Decimal type castable to `Decimal`
static let decimal = DatabaseType(rawValue: DUCKDB_TYPE_DECIMAL.rawValue)
/// Timestamp type castable to `Timestamp`
static let timestampS = DatabaseType(rawValue: DUCKDB_TYPE_TIMESTAMP_S.rawValue)
/// Timestamp type castable to `Timestamp`
static let timestampMS = DatabaseType(rawValue: DUCKDB_TYPE_TIMESTAMP_MS.rawValue)
/// Timestamp type castable to `Timestamp`
static let timestampNS = DatabaseType(rawValue: DUCKDB_TYPE_TIMESTAMP_NS.rawValue)
/// Enum type castable to suitable `Decodable` conforming types
static let `enum` = DatabaseType(rawValue: DUCKDB_TYPE_ENUM.rawValue)
/// Array type castable to suitable `Decodable` conforming types
static let list = DatabaseType(rawValue: DUCKDB_TYPE_LIST.rawValue)
/// Struct type castable to suitable `Decodable` conforming types
static let `struct` = DatabaseType(rawValue: DUCKDB_TYPE_STRUCT.rawValue)
/// Dictionary type castable to suitable `Decodable` conforming types
static let map = DatabaseType(rawValue: DUCKDB_TYPE_MAP.rawValue)
/// Enum type castable to suitable `Decodable` conforming types
static let union = DatabaseType(rawValue: DUCKDB_TYPE_UNION.rawValue)
/// UUID type castable to `UUID`
static let uuid = DatabaseType(rawValue: DUCKDB_TYPE_UUID.rawValue)
}
// MARK: - Internal Types
extension DatabaseType {
static let invalid = DatabaseType(rawValue: DUCKDB_TYPE_INVALID.rawValue)
}
extension DatabaseType: CustomStringConvertible {
public var description: String {
switch self {
case .boolean: return "\(Self.self).boolean"
case .tinyint: return "\(Self.self).tinyint"
case .smallint: return "\(Self.self).smallint"
case .integer: return "\(Self.self).integer"
case .bigint: return "\(Self.self).bigint"
case .utinyint: return "\(Self.self).utinyint"
case .usmallint: return "\(Self.self).usmallint"
case .uinteger: return "\(Self.self).uinteger"
case .ubigint: return "\(Self.self).ubigint"
case .float: return "\(Self.self).float"
case .double: return "\(Self.self).double"
case .timestamp: return "\(Self.self).timestamp"
case .timestampTz: return "\(Self.self).timestampTZ"
case .date: return "\(Self.self).date"
case .time: return "\(Self.self).time"
case .timeTz: return "\(Self.self).timeTZ"
case .interval: return "\(Self.self).interval"
case .hugeint: return "\(Self.self).hugeint"
case .uhugeint: return "\(Self.self).uhugeint"
case .varchar: return "\(Self.self).varchar"
case .blob: return "\(Self.self).blob"
case .decimal: return "\(Self.self).decimal"
case .timestampS: return "\(Self.self).timestampS"
case .timestampMS: return "\(Self.self).timestampMS"
case .timestampNS: return "\(Self.self).timestampNS"
case .`enum`: return "\(Self.self).enum"
case .list: return "\(Self.self).list"
case .`struct`: return "\(Self.self).struct"
case .map: return "\(Self.self).map"
case .union: return "\(Self.self).union"
case .uuid: return "\(Self.self).uuid"
case .invalid: return "\(Self.self).invalid"
default: return "\(Self.self).unknown - id: (\(self.rawValue))"
}
}
}

View File

@@ -0,0 +1,12 @@
# ``DuckDB``
DuckDB is an in-process SQL OLAP database management system
## Overview
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.

View File

@@ -0,0 +1,47 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
#if canImport(TabularData)
import TabularData
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public extension TabularData.Column {
/// Creates a tabular data column initialized from a DuckDB column
init(_ column: DuckDB.Column<WrappedElement>) {
self.init(name: column.name, contents: column)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public extension TabularData.AnyColumn {
/// Creates a type-erased tabular data column from a DuckDB column
init<T>(_ column: DuckDB.Column<T>) {
self = TabularData.Column(column).eraseToAnyColumn()
}
}
#endif

View File

@@ -0,0 +1,33 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
public extension Foundation.Date {
/// Creates a date value initialized from a DuckDB Date value
init(_ date: Date) {
self.init(timeIntervalSince1970: TimeInterval(date.days * 24 * 60 * 60))
}
}

View File

@@ -0,0 +1,73 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
public extension Decimal {
init(_ source: IntHuge) {
let magnitude = source.magnitude
let mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)
let significantBitCount = magnitude.bitWidth - magnitude.leadingZeroBitCount
let length = (significantBitCount + (UInt16.bitWidth - 1)) / UInt16.bitWidth
mantissa.0 = UInt16(truncatingIfNeeded: magnitude >> (0 * 16))
mantissa.1 = UInt16(truncatingIfNeeded: magnitude >> (1 * 16))
mantissa.2 = UInt16(truncatingIfNeeded: magnitude >> (2 * 16))
mantissa.3 = UInt16(truncatingIfNeeded: magnitude >> (3 * 16))
mantissa.4 = UInt16(truncatingIfNeeded: magnitude >> (4 * 16))
mantissa.5 = UInt16(truncatingIfNeeded: magnitude >> (5 * 16))
mantissa.6 = UInt16(truncatingIfNeeded: magnitude >> (6 * 16))
mantissa.7 = UInt16(truncatingIfNeeded: magnitude >> (7 * 16))
self = .init(
_exponent: 0,
_length: UInt32(length),
_isNegative: source.signum() < 0 ? 1 : 0,
_isCompact: 0,
_reserved: 0,
_mantissa: mantissa
)
}
init(_ source: UIntHuge) {
let mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)
let significantBitCount = source.bitWidth - source.leadingZeroBitCount
let length = (significantBitCount + (UInt16.bitWidth - 1)) / UInt16.bitWidth
mantissa.0 = UInt16(truncatingIfNeeded: source >> (0 * 16))
mantissa.1 = UInt16(truncatingIfNeeded: source >> (1 * 16))
mantissa.2 = UInt16(truncatingIfNeeded: source >> (2 * 16))
mantissa.3 = UInt16(truncatingIfNeeded: source >> (3 * 16))
mantissa.4 = UInt16(truncatingIfNeeded: source >> (4 * 16))
mantissa.5 = UInt16(truncatingIfNeeded: source >> (5 * 16))
mantissa.6 = UInt16(truncatingIfNeeded: source >> (6 * 16))
mantissa.7 = UInt16(truncatingIfNeeded: source >> (7 * 16))
self = .init(
_exponent: 0,
_length: UInt32(length),
_isNegative: 0,
_isCompact: 0,
_reserved: 0,
_mantissa: mantissa
)
}
}

View File

@@ -0,0 +1,41 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
public extension Foundation.Date {
/// Creates a date value initialized from a DuckDB Time value
init(_ date: Time) {
self.init(timeIntervalSince1970: TimeInterval(date.microseconds) * 1e-3)
}
}
public extension Time {
/// Creates a time value initialized from a Foundation Date
init(_ date: Foundation.Date) {
self.init(microseconds: Int64(date.timeIntervalSince1970 * 1e3))
}
}

View File

@@ -0,0 +1,41 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
public extension Foundation.Date {
/// Creates a date value initialized from a DuckDB Timestamp value
init(_ timestamp: Timestamp) {
self.init(timeIntervalSince1970: TimeInterval(timestamp.microseconds) * 1e-3)
}
}
public extension Timestamp {
/// Creates a timestamp value initialized from a Foundation Date
init(_ date: Foundation.Date) {
self.init(microseconds: Int64(date.timeIntervalSince1970 * 1e3))
}
}

View File

@@ -0,0 +1,297 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
// MARK: - Type Layouts
struct duckdb_list_entry_t {
let offset: UInt64
let length: UInt64
}
extension duckdb_state {
static let success = duckdb_state(0)
static let failure = duckdb_state(1)
}
extension duckdb_pending_state {
static let ready = duckdb_pending_state(0)
static let notReady = duckdb_pending_state(1)
static let error = duckdb_pending_state(2)
}
// MARK: - Type ID
extension duckdb_type {
var asTypeID: DatabaseType { .init(rawValue: rawValue) }
}
// MARK: - String
extension duckdb_string {
private static let inlineLimit = UInt32(12)
var asString: String {
withUnsafePointer(to: self) { ptr in
let contentsSize = UnsafeRawPointer(ptr).load(as: UInt32.self)
let strPtr: UnsafeRawPointer
if contentsSize <= Self.inlineLimit {
strPtr = UnsafeRawPointer(ptr).advanced(by: MemoryLayout<UInt32>.stride)
}
else {
let strPtrPtr = UnsafeRawPointer(ptr).advanced(by: MemoryLayout<UInt64>.stride)
strPtr = strPtrPtr.load(as: UnsafeRawPointer.self)
}
let stringData = Data(bytes: strPtr, count: Int(contentsSize))
return String(data: stringData, encoding:.utf8)!
}
}
}
// MARK: - Huge Int
extension duckdb_hugeint {
init(_ source: IntHuge) {
self = duckdb_hugeint(lower: source.low, upper: source.high)
}
var asIntHuge: IntHuge { .init(high: upper, low: lower) }
var asUIntHuge: UIntHuge {
let high = IntHuge(upper) + IntHuge(Int64.max) + 1
return UIntHuge(high) << 64 + UIntHuge(lower)
}
var asUUID: UUID {
let value = asUIntHuge
let uuid = UUID(
uuid: uuid_t(
UInt8(truncatingIfNeeded: value >> (8 * 15)),
UInt8(truncatingIfNeeded: value >> (8 * 14)),
UInt8(truncatingIfNeeded: value >> (8 * 13)),
UInt8(truncatingIfNeeded: value >> (8 * 12)),
UInt8(truncatingIfNeeded: value >> (8 * 11)),
UInt8(truncatingIfNeeded: value >> (8 * 10)),
UInt8(truncatingIfNeeded: value >> (8 * 9)),
UInt8(truncatingIfNeeded: value >> (8 * 8)),
UInt8(truncatingIfNeeded: value >> (8 * 7)),
UInt8(truncatingIfNeeded: value >> (8 * 6)),
UInt8(truncatingIfNeeded: value >> (8 * 5)),
UInt8(truncatingIfNeeded: value >> (8 * 4)),
UInt8(truncatingIfNeeded: value >> (8 * 3)),
UInt8(truncatingIfNeeded: value >> (8 * 2)),
UInt8(truncatingIfNeeded: value >> (8 * 1)),
UInt8(truncatingIfNeeded: value >> (8 * 0))
)
)
return uuid
}
}
// MARK: - Unsigned Huge Int
extension duckdb_uhugeint {
init(_ source: UIntHuge) {
self = duckdb_uhugeint(lower: source.low, upper: source.high)
}
var asUIntHuge: UIntHuge { .init(high: upper, low: lower) }
}
// MARK: - Time
extension duckdb_time {
init(time: Time) { self = duckdb_time(micros: time.microseconds) }
var asTime: Time { Time(microseconds: micros) }
}
extension duckdb_time_struct {
init(components: Time.Components) {
self = duckdb_time_struct(
hour: components.hour,
min: components.minute,
sec: components.second,
micros: components.microsecond
)
}
var asTimeComponents: Time.Components {
.init(hour: hour, minute: min, second: sec, microsecond: micros)
}
}
extension duckdb_time_tz {
var asTime: TimeTz {
let res = duckdb_from_time_tz(self)
let ctimestruct = duckdb_to_time(res.time);
return TimeTz(time: Time(microseconds: ctimestruct.micros), offset: res.offset)
}
}
// MARK: - Date
extension duckdb_date {
init(date: Date) { self = duckdb_date(days: date.days) }
var asDate: Date { Date(days: days) }
}
extension duckdb_date_struct {
init(components: Date.Components) {
self = duckdb_date_struct(year: components.year, month: components.month, day: components.day)
}
var asDateComponents: Date.Components {
.init(year: year, month: month, day: day)
}
}
// MARK: - Timestamp
extension duckdb_timestamp {
init(timestamp: Timestamp) { self = duckdb_timestamp(micros: timestamp.microseconds) }
var asTimestamp: Timestamp { Timestamp(microseconds: micros) }
}
extension duckdb_timestamp_struct {
init(components: Timestamp.Components) {
self = duckdb_timestamp_struct(
date: duckdb_date_struct(components: components.date),
time: duckdb_time_struct(components: components.time)
)
}
var asTimestampComponents: Timestamp.Components {
.init(date: date.asDateComponents, time: time.asTimeComponents)
}
}
// MARK: - Interval
extension duckdb_interval {
init(interval: Interval) {
self = duckdb_interval(
months: interval.months, days: interval.days, micros: interval.microseconds)
}
var asInterval: Interval {
Interval(months: months, days: days, microseconds: micros)
}
}
// MARK: - Blob
extension duckdb_blob {
private static let inlineLimit = UInt32(12)
var asData: Data {
withUnsafePointer(to: self) { ptr in
let contentsSize = UnsafeRawPointer(ptr).load(as: UInt32.self)
let blobPtr: UnsafeRawPointer
if contentsSize <= Self.inlineLimit {
blobPtr = UnsafeRawPointer(ptr).advanced(by: MemoryLayout<UInt32>.stride)
}
else {
let blobPtrPtr = UnsafeRawPointer(ptr).advanced(by: MemoryLayout<UInt64>.stride)
blobPtr = blobPtrPtr.load(as: UnsafeRawPointer.self)
}
return Data(bytes: blobPtr, count: Int(contentsSize))
}
}
}
// MARK: - Decimal
extension duckdb_decimal {
private static let scaleLimit = 38
init(_ source: Decimal) throws {
let mantissaLimit = source.isSignMinus ? 1 + UIntHuge(IntHuge.max) : UIntHuge(IntHuge.max)
var scale: Int
var mantissa: UIntHuge
if source.exponent > 0 {
let exponent = UIntHuge(source.exponent)
let (out, overflow) = source.hugeMantissa.multipliedReportingOverflow(by: exponent)
guard overflow == false else { throw DatabaseError.decimalUnrepresentable }
mantissa = out
scale = 0
}
else {
scale = -source.exponent
mantissa = source.hugeMantissa
while scale > Self.scaleLimit {
mantissa /= 10
scale -= 1
}
while scale > 0, mantissa > mantissaLimit {
mantissa /= 10
scale -= 1
}
}
guard mantissa <= mantissaLimit else {
throw DatabaseError.decimalUnrepresentable
}
guard mantissa > 0 else {
self = duckdb_decimal(width: 0, scale: 0, value: .init(lower: 0, upper: 0))
return
}
let value = source.isSignMinus
? IntHuge.min + IntHuge(mantissaLimit - mantissa) : IntHuge(mantissa)
self = duckdb_decimal(width: 38, scale: .init(scale), value: .init(value))
}
}
fileprivate extension Decimal {
var hugeMantissa: UIntHuge {
let components = [
_mantissa.0, _mantissa.1, _mantissa.2, _mantissa.3,
_mantissa.4, _mantissa.5, _mantissa.6, _mantissa.7
]
var mantissa = UIntHuge(0)
for i in 0..<Int(_length) {
mantissa += UIntHuge(components[i]) << (i * 16)
}
return mantissa
}
}
// MARK: - Vector
extension duckdb_vector {
var logicalType: LogicalType {
LogicalType { duckdb_vector_get_column_type(self) }
}
}

View File

@@ -0,0 +1,47 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
final class DataChunk: Sendable {
var count: DBInt { duckdb_data_chunk_get_size(ptr.pointee) }
var columnCount: DBInt { duckdb_data_chunk_get_column_count(ptr.pointee) }
private let ptr = UnsafeMutablePointer<duckdb_data_chunk?>.allocate(capacity: 1)
init(cresult: duckdb_result, index: DBInt) {
self.ptr.pointee = duckdb_result_get_chunk(cresult, index)!
}
deinit {
duckdb_destroy_data_chunk(ptr)
ptr.deallocate()
}
func withVector<T>(at index: DBInt, _ body: (Vector) throws -> T) rethrows -> T {
try body(Vector(duckdb_data_chunk_get_vector(ptr.pointee, index), count: Int(count)))
}
}

View File

@@ -0,0 +1,48 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
protocol DecimalStorageType {
var asDecimal: Decimal { get }
}
extension Int8: DecimalStorageType {
var asDecimal: Decimal { Decimal(self) }
}
extension Int16: DecimalStorageType {
var asDecimal: Decimal { Decimal(self) }
}
extension Int32: DecimalStorageType {
var asDecimal: Decimal { Decimal(self) }
}
extension Int64: DecimalStorageType {
var asDecimal: Decimal { Decimal(self) }
}
extension IntHuge: DecimalStorageType {
var asDecimal: Decimal { Decimal(self) }
}

View File

@@ -0,0 +1,33 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
extension Optional where Wrapped: StringProtocol {
func withOptionalCString<T>(_ body: (UnsafePointer<CChar>?) throws -> T) rethrows -> T {
guard let string = self else { return try body(nil) }
return try string.withCString(body)
}
}

View File

@@ -0,0 +1,71 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
protocol PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { get }
}
extension Bool: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .boolean }
}
extension Int8: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .tinyint }
}
extension Int16: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .smallint }
}
extension Int32: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .integer }
}
extension Int64: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .bigint }
}
extension UInt8: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .utinyint }
}
extension UInt16: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .usmallint }
}
extension UInt32: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .uinteger }
}
extension UInt64: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .ubigint }
}
extension Float: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .float }
}
extension Double: PrimitiveDatabaseValue {
static var representedDatabaseTypeID: DatabaseType { .double }
}

View File

@@ -0,0 +1,340 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
struct Vector {
static let vectorSize = DBInt(duckdb_vector_size())
let count: Int
let offset: Int
let logicalType: LogicalType
private let cvector: duckdb_vector
init(_ cvector: duckdb_vector, count: Int, offset: Int = 0, logicalType: LogicalType? = nil) {
self.count = count
self.offset = offset
self.cvector = cvector
self.logicalType = logicalType ?? cvector.logicalType
}
func withCVector<T>(_ body: (duckdb_vector) throws -> T) rethrows -> T {
try body(cvector)
}
}
extension Vector {
func unwrapNull(at index: Int) -> Bool {
precondition(index < count, "vector index out of bounds")
let offsetIndex = offset + index
let validityMasksPtr = duckdb_vector_get_validity(cvector)
guard let validityMasksPtr else { return false }
let validityMaskEntryIndex = offsetIndex / 64
let validityMaskEntryPtr = (validityMasksPtr + validityMaskEntryIndex)
let validityBitIndex = offsetIndex % 64
let validityBit = (DBInt(1) << validityBitIndex)
return validityMaskEntryPtr.pointee & validityBit == 0
}
func unwrap(_ type: Int.Type, at index: Int) throws -> Int {
switch logicalType.dataType {
case .tinyint:
return Int(try unwrap(Int8.self, at: index))
case .smallint:
return Int(try unwrap(Int16.self, at: index))
case .integer:
return Int(try unwrap(Int32.self, at: index))
case .bigint:
return Int(try unwrap(Int64.self, at: index))
default:
throw DatabaseError.typeMismatch(Int.self)
}
}
func unwrap(_ type: UInt.Type, at index: Int) throws -> UInt {
switch logicalType.dataType {
case .utinyint:
return UInt(try unwrap(UInt8.self, at: index))
case .usmallint:
return UInt(try unwrap(UInt16.self, at: index))
case .uinteger:
return UInt(try unwrap(UInt32.self, at: index))
case .ubigint:
return UInt(try unwrap(UInt64.self, at: index))
default:
throw DatabaseError.typeMismatch(Int.self)
}
}
func unwrap<T: PrimitiveDatabaseValue>(_ type: T.Type, at index: Int) throws -> T {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: T.representedDatabaseTypeID)
return unsafelyUnwrapElement(as: T.self, at: index)
}
func unwrap(_ type: String.Type, at index: Int) throws -> String {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .varchar)
return unsafelyUnwrapElement(as: duckdb_string.self, at: index) { $0.asString }
}
func unwrap(_ type: IntHuge.Type, at index: Int) throws -> IntHuge {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .hugeint)
return unsafelyUnwrapElement(as: duckdb_hugeint.self, at: index) { $0.asIntHuge }
}
func unwrap(_ type: UIntHuge.Type, at index: Int) throws -> UIntHuge {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .uhugeint)
return unsafelyUnwrapElement(as: duckdb_uhugeint.self, at: index) { $0.asUIntHuge }
}
func unwrap(_ type: UUID.Type, at index: Int) throws -> UUID {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .uuid)
return unsafelyUnwrapElement(as: duckdb_hugeint.self, at: index) { $0.asUUID }
}
func unwrap(_ type: Time.Type, at index: Int) throws -> Time {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .time)
return unsafelyUnwrapElement(as: duckdb_time.self, at: index) { $0.asTime }
}
func unwrap(_ type: TimeTz.Type, at index: Int) throws -> TimeTz {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .timeTz)
return unsafelyUnwrapElement(as: duckdb_time_tz.self, at: index) { $0.asTime }
}
func unwrap(_ type: Date.Type, at index: Int) throws -> Date {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .date)
return unsafelyUnwrapElement(as: duckdb_date.self, at: index) { $0.asDate }
}
func unwrap(_ type: Interval.Type, at index: Int) throws -> Interval {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .interval)
return unsafelyUnwrapElement(as: duckdb_interval.self, at: index) { $0.asInterval }
}
func unwrap(_ type: Timestamp.Type, at index: Int) throws -> Timestamp {
let columnTypes = [DatabaseType.timestampS, .timestampMS, .timestamp, .timestampTz, .timestampNS]
try assertNonNullTypeMatch(of: type, at: index, withColumnTypes: .init(columnTypes))
return unsafelyUnwrapElement(as: duckdb_timestamp.self, at: index) { ctimestamp in
switch logicalType.dataType {
case .timestampS:
let scaled = duckdb_timestamp(micros: ctimestamp.micros * 1_000_000)
return scaled.asTimestamp
case .timestampMS:
let scaled = duckdb_timestamp(micros: ctimestamp.micros * 1_000)
return scaled.asTimestamp
case .timestampNS:
let scaled = duckdb_timestamp(micros: ctimestamp.micros / 1_000)
return scaled.asTimestamp
default:
return ctimestamp.asTimestamp
}
}
}
func unwrap(_ type: Data.Type, at index: Int) throws -> Data {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .blob)
return unsafelyUnwrapElement(as: duckdb_blob.self, at: index) { $0.asData }
}
func unwrap(_ type: Decimal.Type, at index: Int) throws -> Decimal {
try assertNonNullTypeMatch(of: type, at: index, withColumnType: .decimal)
guard let props = logicalType.decimalProperties else {
fatalError("expected decimal logical type")
}
switch props.storageType {
case .tinyint:
return unwrapDecimal(withUnderlyingType: Int8.self, scale: props.scale, at: index)
case .smallint:
return unwrapDecimal(withUnderlyingType: Int16.self, scale: props.scale, at: index)
case .integer:
return unwrapDecimal(withUnderlyingType: Int32.self, scale: props.scale, at: index)
case .bigint:
return unwrapDecimal(withUnderlyingType: Int64.self, scale: props.scale, at: index)
case .hugeint:
return unwrapDecimal(withUnderlyingType: IntHuge.self, scale: props.scale, at: index)
case let unexpectedInternalType:
fatalError("unexpected internal decimal type: \(unexpectedInternalType)")
}
}
func unwrapDecimal<T: DecimalStorageType>(
withUnderlyingType storageType: T.Type, scale: UInt8, at index: Int
) -> Decimal {
unsafelyUnwrapElement(as: T.self, at: index) { decimalStorage in
let storageValue = decimalStorage.asDecimal
let sign = storageValue.sign
let exponent = -Int(scale)
let significand = abs(storageValue)
return Decimal(sign: sign, exponent: exponent, significand: significand)
}
}
func unsafelyUnwrapElement<T>(as type: T.Type, at index: Int) -> T {
unsafelyUnwrapElement(as: type, at: index) { $0 }
}
func unsafelyUnwrapElement<T, U>(
as type: T.Type, at index: Int, transform: (T) -> U
) -> U {
precondition(index < count, "vector index out of bounds")
let offsetIndex = offset + index
let dataPtr = duckdb_vector_get_data(cvector)!
let itemDataPtr = dataPtr.assumingMemoryBound(to: T.self)
return transform(itemDataPtr.advanced(by: offsetIndex)[0])
}
func assertNonNullTypeMatch<T>(
of type: T.Type, at index: Int, withColumnType columnType: DatabaseType
) throws {
try assertNonNullTypeMatch(of: type, at: index, withColumnTypes: .init([columnType]))
}
func assertNonNullTypeMatch<T>(
of type: T.Type, at index: Int, withColumnTypes columnTypes: Set<DatabaseType>
) throws {
guard unwrapNull(at: index) == false else {
throw DatabaseError.valueNotFound(type)
}
guard columnTypes.contains(logicalType.underlyingDataType) else {
throw DatabaseError.typeMismatch(type)
}
}
}
// MARK: - Collection Conformance
extension Vector: Collection {
struct Element {
let vector: Vector
let index: Int
}
public struct Iterator: IteratorProtocol {
private let vector: Vector
private var position: Int
init(_ vector: Vector) {
self.vector = vector
self.position = 0
}
public mutating func next() -> Element? {
guard position < vector.count else { return nil }
defer { position += 1 }
return vector[position]
}
}
public var startIndex: Int { 0 }
public var endIndex: Int { count }
public subscript(position: Int) -> Element { Element(vector: self, index: position) }
public func makeIterator() -> Iterator { Iterator(self) }
public func index(after i: Int) -> Int { i + 1 }
public func index(before i: Int) -> Int { i - 1 }
}
// MARK: - Element Accessors
extension Vector.Element {
var dataType: DatabaseType { vector.logicalType.dataType }
var logicalType: LogicalType { vector.logicalType }
func unwrapNull() -> Bool { vector.unwrapNull(at: index) }
func unwrap(_ type: Int.Type) throws -> Int { try vector.unwrap(type, at: index) }
func unwrap(_ type: UInt.Type) throws -> UInt { try vector.unwrap(type, at: index) }
func unwrap<T: PrimitiveDatabaseValue>(_ type: T.Type) throws -> T { try vector.unwrap(type, at: index) }
func unwrap(_ type: String.Type) throws -> String { try vector.unwrap(type, at: index) }
func unwrap(_ type: IntHuge.Type) throws -> IntHuge { try vector.unwrap(type, at: index) }
func unwrap(_ type: UIntHuge.Type) throws -> UIntHuge { try vector.unwrap(type, at: index) }
func unwrap(_ type: UUID.Type) throws -> UUID { try vector.unwrap(type, at: index) }
func unwrap(_ type: Time.Type) throws -> Time { try vector.unwrap(type, at: index) }
func unwrap(_ type: Date.Type) throws -> Date { try vector.unwrap(type, at: index) }
func unwrap(_ type: Interval.Type) throws -> Interval { try vector.unwrap(type, at: index) }
func unwrap(_ type: Timestamp.Type) throws -> Timestamp { try vector.unwrap(type, at: index) }
func unwrap(_ type: Data.Type) throws -> Data { try vector.unwrap(type, at: index) }
func unwrap(_ type: Decimal.Type) throws -> Decimal { try vector.unwrap(type, at: index) }
func unwrap(_ type: TimeTz.Type) throws -> TimeTz { try vector.unwrap(type, at: index) }
func unwrapDecodable<T: Decodable>(_ type: T.Type) throws -> T {
try VectorElementDecoder.default.decode(T.self, element: self)
}
}
// MARK: - Map Contents accessors
extension Vector.Element {
struct MapContent {
let keyVector: Vector
let valueVector: Vector
}
var childVector: Vector? {
guard let child = duckdb_list_vector_get_child(vector.cvector) else { return nil }
let count = duckdb_list_vector_get_size(vector.cvector)
let info = vector.unsafelyUnwrapElement(as: duckdb_list_entry_t.self, at: vector.offset + index)
precondition(info.offset + info.length <= count)
return Vector(child, count: Int(info.length), offset: Int(info.offset))
}
var mapContents: MapContent? {
guard dataType == .map else { return nil }
guard let childVector = childVector else { return nil }
guard let keys = duckdb_struct_vector_get_child(childVector.cvector, 0) else { return nil }
guard let values = duckdb_struct_vector_get_child(childVector.cvector, 1) else { return nil }
let keyVector = Vector(keys, count: childVector.count, offset: childVector.offset)
let valueVector = Vector(values, count: childVector.count, offset: childVector.offset)
return MapContent(keyVector: keyVector, valueVector: valueVector)
}
}
// MARK: - Struct Contents accesors
extension Vector.Element {
struct StructMemberContent {
let name: String
let vector: Vector
}
var structContents: [StructMemberContent]? {
guard let properties = vector.logicalType.structMemberProperties else { return nil }
var content = [StructMemberContent]()
for (i, member) in properties.enumerated() {
let memberCVector = duckdb_struct_vector_get_child(vector.cvector, DBInt(i))!
let memberVector = Vector(memberCVector, count: vector.count, offset: vector.offset)
content.append(.init(name: member.name, vector: memberVector))
}
return content
}
}

View File

@@ -0,0 +1,547 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
// MARK: - Top Level Decoder
final class VectorElementDecoder {
static let `default` = VectorElementDecoder()
func decode<T: Decodable>(_ type: T.Type, element: Vector.Element) throws -> T {
try T(from: VectorElementDataDecoder(element: element))
}
}
// MARK: - Vector Decoder
fileprivate struct VectorElementDataDecoder: Decoder {
struct VectorDecoderCodingKey: CodingKey {
let stringValue: String
let intValue: Int?
init(stringValue: String) {
self.intValue = nil
self.stringValue = stringValue
}
init(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
let codingPath: [CodingKey]
let element: Vector.Element
let userInfo: [CodingUserInfoKey : Any]
init(element: Vector.Element, codingPath: [CodingKey] = []) {
self.codingPath = codingPath
self.element = element
self.userInfo = [CodingUserInfoKeys.logicalTypeCodingUserInfoKey: element.logicalType]
}
func container<Key: CodingKey>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
switch element.dataType {
case .map:
return .init(
try KeyedValueContainer<Key>.createMapContainer(decoder: self, element: element)
)
case .struct:
return .init(
try KeyedValueContainer<Key>.createStructContainer(decoder: self, element: element)
)
default:
let columnType = element.dataType
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot create keyed decoding container for column type \(columnType)"
)
throw DecodingError.typeMismatch(type, context)
}
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
switch element.dataType {
case .list:
return try UnkeyedValueContainer.createListContainer(decoder: self, element: element)
default:
let columnType = element.dataType
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot create unkeyed decoding container for column type \(columnType)"
)
throw DecodingError.typeMismatch(UnkeyedDecodingContainer.self, context)
}
}
func singleValueContainer() -> SingleValueDecodingContainer {
SingleValueContainer(decoder: self, codingPath: codingPath, element: element)
}
}
// MARK: - Single Value Container
extension VectorElementDataDecoder {
struct SingleValueContainer: SingleValueDecodingContainer {
let decoder: VectorElementDataDecoder
var codingPath: [CodingKey]
let element: Vector.Element
init(decoder: VectorElementDataDecoder, codingPath: [CodingKey], element: Vector.Element) {
self.decoder = decoder
self.codingPath = codingPath
self.element = element
}
// Protocol conforming `decode(_:)` implementations
func decodeNil() -> Bool {
element.unwrapNull()
}
func decode(_ type: Int.Type) throws -> Int {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Int8.Type) throws -> Int8 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Int16.Type) throws -> Int16 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Int32.Type) throws -> Int32 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Int64.Type) throws -> Int64 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UInt.Type) throws -> UInt {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UInt8.Type) throws -> UInt8 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UInt16.Type) throws -> UInt16 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UInt32.Type) throws -> UInt32 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UInt64.Type) throws -> UInt64 {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Float.Type) throws -> Float {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Double.Type) throws -> Double {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: String.Type) throws -> String {
try attemptDecode { try element.unwrap(type) }
}
// Special case `decode(_:)` implementations
func decode(_ type: IntHuge.Type) throws -> IntHuge {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UIntHuge.Type) throws -> UIntHuge {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Decimal.Type) throws -> Decimal {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Data.Type) throws -> Data {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: UUID.Type) throws -> UUID {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Date.Type) throws -> Date {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Time.Type) throws -> Time {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: TimeTz.Type) throws -> TimeTz {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Timestamp.Type) throws -> Timestamp {
try attemptDecode { try element.unwrap(type) }
}
func decode(_ type: Interval.Type) throws -> Interval {
try attemptDecode { try element.unwrap(type) }
}
// Generic decode
func decode<T: Decodable>(_ type: T.Type) throws -> T {
switch type {
case is IntHuge.Type:
return try decode(IntHuge.self) as! T
case is UIntHuge.Type:
return try decode(UIntHuge.self) as! T
case is Decimal.Type:
return try decode(Decimal.self) as! T
case is Data.Type:
return try decode(Data.self) as! T
case is UUID.Type:
return try decode(UUID.self) as! T
case is Date.Type:
return try decode(Date.self) as! T
case is Time.Type:
return try decode(Time.self) as! T
case is Timestamp.Type:
return try decode(Timestamp.self) as! T
case is Interval.Type:
return try decode(Interval.self) as! T
case is TimeTz.Type:
return try decode(TimeTz.self) as! T
default:
return try T(from: decoder)
}
}
private func attemptDecode<T>(_ body: () throws -> T) throws -> T {
do {
return try body()
}
catch DatabaseError.valueNotFound(let type) {
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected value of type \(type) found null value instead"
)
throw DecodingError.valueNotFound(type, context)
}
catch DatabaseError.typeMismatch(let type) {
let columnType = element.dataType
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected to decode \(type) but found \(columnType) instead."
)
throw DecodingError.typeMismatch(type, context)
}
}
}
}
// MARK: - Unkeyed Container
extension VectorElementDataDecoder {
struct UnkeyedValueContainer: UnkeyedDecodingContainer {
let decoder: VectorElementDataDecoder
let codingPath: [CodingKey]
let vector: Vector
let count: Int?
var currentIndex = 0
var isAtEnd: Bool { currentIndex >= vector.count }
init(
decoder: VectorElementDataDecoder,
codingPath: [CodingKey],
vector: Vector
) {
self.decoder = decoder
self.codingPath = codingPath
self.vector = vector
self.count = vector.count
}
func decodeNil() -> Bool {
vector[currentIndex].unwrapNull()
}
mutating func decode<T: Decodable>(_ type: T.Type) throws -> T {
let decoder = try superDecoder()
let value = try T(from: decoder)
currentIndex += 1
return value
}
func nestedContainer<NestedKey: CodingKey>(
keyedBy type: NestedKey.Type
) throws -> KeyedDecodingContainer<NestedKey> {
let decoder = try superDecoder()
return try decoder.container(keyedBy: type)
}
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
let decoder = try superDecoder()
return try decoder.unkeyedContainer()
}
func superDecoder() throws -> Decoder {
var codingPath = decoder.codingPath
codingPath.append(VectorDecoderCodingKey(intValue: currentIndex))
return VectorElementDataDecoder(element: vector[currentIndex], codingPath: codingPath)
}
}
}
// MARK: - Keyed Container
extension VectorElementDataDecoder {
struct KeyedValueContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
struct Properties {
let keyMap: [String: KeyPath]
let vectors: [Vector]
}
struct KeyPath {
let key: Key
let vectorIndex: Int
let rowIndex: Int
}
let decoder: VectorElementDataDecoder
let codingPath: [CodingKey]
let keyMap: [String: KeyPath]
let vectors: [Vector]
init(
decoder: VectorElementDataDecoder,
codingPath: [CodingKey],
keyMap: [String: KeyPath],
vectors: [Vector]
) {
self.decoder = decoder
self.codingPath = codingPath
self.keyMap = keyMap
self.vectors = vectors
}
var allKeys: [Key] { keyMap.values.map { $0.key } }
func contains(_ key: Key) -> Bool {
keyMap[key.stringValue] != nil
}
func decodeNil(forKey key: Key) throws -> Bool {
let path = try unwrapPath(forKey: key)
let vector = vectors[path.vectorIndex]
return vector[path.rowIndex].unwrapNull()
}
func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
let decoder = try superDecoder(forKey: key)
return try T(from: decoder)
}
func nestedContainer<NestedKey: CodingKey>(
keyedBy type: NestedKey.Type, forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
let decoder = try superDecoder(forKey: key)
return try decoder.container(keyedBy: type)
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
let decoder = try superDecoder(forKey: key)
return try decoder.unkeyedContainer()
}
func superDecoder() throws -> Decoder {
VectorElementDataDecoder(element: decoder.element, codingPath: decoder.codingPath)
}
func superDecoder(forKey key: Key) throws -> Decoder {
let path = try unwrapPath(forKey: key)
let vector = vectors[path.vectorIndex]
var codingPath = decoder.codingPath
codingPath.append(VectorDecoderCodingKey(stringValue: key.stringValue))
return VectorElementDataDecoder(element: vector[path.rowIndex], codingPath: codingPath)
}
private func unwrapPath(forKey key: Key) throws -> KeyPath {
guard let path = keyMap[key.stringValue] else {
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Key not found"
)
throw DecodingError.keyNotFound(key, context)
}
return path
}
}
}
// MARK: - Container Factories
fileprivate extension VectorElementDataDecoder.KeyedValueContainer {
static func createMapContainer(
decoder: VectorElementDataDecoder, element: Vector.Element
) throws -> Self {
guard element.unwrapNull() == false else {
let context = DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Cannot get keyed decoding container found null value instead"
)
throw DecodingError.valueNotFound(Self.self, context)
}
guard element.dataType == .map else {
let columnType = element.dataType
let context = DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected map column type, found \(columnType) column type instead."
)
throw DecodingError.typeMismatch(Self.self, context)
}
guard let mapContent = element.mapContents else {
fatalError("Internal consistency error. Expected map content in vector.")
}
let keys: [Key]
switch mapContent.keyVector.logicalType.dataType {
case .varchar:
let stringKeys = try mapContent.keyVector.map { try $0.unwrap(String.self) }
keys = try stringKeys.map { try createKey(stringValue: $0) }
case .tinyint, .smallint, .integer, .bigint:
let intKeys = try mapContent.keyVector.map { try $0.unwrap(Int.self) }
keys = try intKeys.map { try createKey(intValue: $0) }
default:
let context = DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Cannot decode map with non string/integer key type"
)
throw DecodingError.typeMismatch(Key.self, context)
}
let vectors = [mapContent.valueVector]
let mapKeys = keys.map(\.stringValue)
let mapValues = keys.enumerated().map { KeyPath(key: $0.1, vectorIndex: 0, rowIndex: $0.0) }
let keyMap = Dictionary(uniqueKeysWithValues: zip(mapKeys, mapValues))
return Self(decoder: decoder, codingPath: decoder.codingPath, keyMap: keyMap, vectors: vectors)
}
static func createStructContainer(
decoder: VectorElementDataDecoder, element: Vector.Element
) throws -> Self {
let codingPath = decoder.codingPath
let dataType = element.dataType
guard element.unwrapNull() == false else {
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get keyed decoding container found null value instead"
)
throw DecodingError.valueNotFound(Self.self, context)
}
guard dataType == .struct else {
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected struct column type, found \(dataType) column type instead."
)
throw DecodingError.typeMismatch(Self.self, context)
}
guard let structContents = element.structContents else {
fatalError("Internal consistency error. Expected struct content in vector.")
}
let keys = try structContents.map(\.name).map(Self.createKey(stringValue:))
let vectors = structContents.map(\.vector)
var keyMap = [String: KeyPath]()
for (i, key) in keys.enumerated() {
keyMap[key.stringValue] = .init(key: key, vectorIndex: i, rowIndex: element.index)
}
return Self(decoder: decoder, codingPath: codingPath, keyMap: keyMap, vectors: vectors)
}
private static func createKey(stringValue: String) throws -> Key {
guard let key = Key(stringValue: stringValue) else {
let context = DecodingError.Context(
codingPath: [],
debugDescription: "Cannot instatiate key of type \(Key.self) with string: \(stringValue)"
)
throw DecodingError.typeMismatch(Key.self, context)
}
return key
}
private static func createKey(intValue: Int) throws -> Key {
guard let key = Key(intValue: intValue) else {
let context = DecodingError.Context(
codingPath: [],
debugDescription: "Cannot instatiate key of type \(Key.self) with integer: \(intValue)"
)
throw DecodingError.typeMismatch(Key.self, context)
}
return key
}
}
fileprivate extension VectorElementDataDecoder.UnkeyedValueContainer {
static func createListContainer(
decoder: VectorElementDataDecoder, element: Vector.Element
) throws -> Self {
let codingPath = decoder.codingPath
let dataType = element.dataType
guard element.unwrapNull() == false else {
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get unkeyed decoding container found null value instead"
)
throw DecodingError.valueNotFound(Self.self, context)
}
guard dataType == .list else {
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected list column type, found \(dataType) column type instead."
)
throw DecodingError.typeMismatch(Self.self, context)
}
guard let childVector = element.childVector else {
fatalError("Internal consistency error. Expected list content in vector.")
}
return Self(decoder: decoder, codingPath: codingPath, vector: childVector)
}
}

View File

@@ -0,0 +1,163 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
public final class LogicalType {
private let ptr = UnsafeMutablePointer<duckdb_logical_type?>.allocate(capacity: 1)
init(_ body: () -> duckdb_logical_type?) {
self.ptr.pointee = body()
}
deinit {
duckdb_destroy_logical_type(ptr)
ptr.deallocate()
}
/// The primitive type represented by this logical type.
public var dataType: DatabaseType {
let ctypeid = duckdb_get_type_id(ptr.pointee)
return ctypeid.asTypeID
}
/// The primitive type representing the internal storage type, which is equivalent
/// to ``dataType``, except for when the type is an enum.
public var underlyingDataType: DatabaseType {
guard dataType == .enum else { return dataType }
let ctypeid = duckdb_enum_internal_type(ptr.pointee)
return ctypeid.asTypeID
}
}
// MARK: - Decimal
public extension LogicalType {
/// Properties associated with a decimal type
struct DecimalProperties {
let width: UInt8
let scale: UInt8
let storageType: DatabaseType
}
/// Properties associated with a decimal type. For all other types, returns `nil`
var decimalProperties: DecimalProperties? {
guard dataType == .decimal else { return nil }
let internalStorageType = duckdb_decimal_internal_type(ptr.pointee)
return .init(
width: duckdb_decimal_width(ptr.pointee),
scale: duckdb_decimal_scale(ptr.pointee),
storageType: internalStorageType.asTypeID
)
}
}
// MARK: - Struct
public extension LogicalType {
static let structCompatibleTypes = [DatabaseType.struct, .map]
/// Properties associated with a struct type
struct StructMemberProperties {
let name: String
let type: LogicalType
}
/// Properties associated with a struct type. For all other types, returns `nil`
var structMemberProperties: [StructMemberProperties]? {
guard Self.structCompatibleTypes.contains(dataType) else { return nil }
let memberCount = duckdb_struct_type_child_count(ptr.pointee)
var properties = [StructMemberProperties]()
properties.reserveCapacity(Int(memberCount))
for i in 0..<memberCount {
let cStr = duckdb_struct_type_child_name(ptr.pointee, i)!
properties.append(StructMemberProperties(
name: String(cString: cStr),
type: LogicalType { duckdb_struct_type_child_type(ptr.pointee, i) }
))
duckdb_free(cStr)
}
return properties
}
}
// MARK: Union
public extension LogicalType {
/// Properties associated with a union type
struct UnionMemberProperties {
let name: String
let type: LogicalType
}
/// Properties associated with a union type. For all other types, returns `nil`
var unionMemberProperties: [UnionMemberProperties]? {
guard dataType == .union else { return nil }
let memberCount = duckdb_union_type_member_count(ptr.pointee)
var properties = [UnionMemberProperties]()
properties.reserveCapacity(Int(memberCount))
for i in 0..<memberCount {
let cStr = duckdb_union_type_member_name(ptr.pointee, i)!
properties.append(UnionMemberProperties(
name: String(cString: cStr),
type: LogicalType { duckdb_union_type_member_type(ptr.pointee, i) }
))
duckdb_free(cStr)
}
return properties
}
}
// MARK: - List
public extension LogicalType {
/// Child type of a list type. For all other types, returns `nil`
var listChildType: LogicalType? {
guard dataType == .list else { return nil }
return LogicalType { duckdb_list_type_child_type(ptr.pointee) }
}
}
// MARK: - Map
public extension LogicalType {
/// Key type of a map type. For all other types, returns `nil`
var mapKeyType: LogicalType? {
guard dataType == .map else { return nil }
return LogicalType { duckdb_map_type_key_type(ptr.pointee) }
}
/// Value type of a map type. For all other types, returns `nil`
var mapValueType: LogicalType? {
guard dataType == .map else { return nil }
return LogicalType { duckdb_map_type_value_type(ptr.pointee) }
}
}

View File

@@ -0,0 +1,458 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
/// An object representing a DuckDB prepared statement
///
/// A prepared statement is a parameterized query. The query is prepared with
/// question marks (`?`) or dollar symbols (`$1`) indicating the parameters of
/// the query. Values can then be bound to these parameters, after which the
/// prepared statement can be executed using those parameters. A single query
/// can be prepared once and executed many times.
///
/// Prepared statements are useful to:
///
/// - Easily supply parameters to functions while avoiding string
/// concatenation/SQL injection attacks.
/// - Speed up queries that will be executed many times with different
/// parameters.
///
/// The following example creates a prepared statement that allows parameters
/// to be bound in two positions within a 'select' statement. The prepared
/// statement is finally executed by calling ``PreparedStatement/execute()``.
///
/// ```swift
/// let connection: Connection = ...
/// let statement = try PreparedStatement(
/// connection: connection,
/// query: "SELECT $1 from $2"
/// )
/// try statement.bind("last_name")
/// try statement.bind("employees")
/// // executes 'SELECT last_name from employees'
/// let result = try statement.execute()
/// ```
public final class PreparedStatement {
/// The number of parameters to which values can be bound
public var parameterCount: Int { Int(duckdb_nparams(ptr.pointee)) }
private let connection: Connection
private let ptr = UnsafeMutablePointer<duckdb_prepared_statement?>.allocate(capacity: 1)
/// Creates a new prepared statement for a given connection and query
///
/// The query is prepared with question marks (`?`) or dollar symbols (`$1`)
/// indicating the parameters of the query.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter connection: the connection on which the prepared stement will
/// execute
/// - Parameter query: the parameterized query
/// - Throws: ``DatabaseError/preparedStatementFailedToInitialize(reason:)``
/// if there is a problem with the query or connection
public init(connection: Connection, query: String) throws {
self.connection = connection
let status = query.withCString { queryStPtr in
connection.withCConnection { duckdb_prepare($0, queryStPtr, ptr) }
}
guard .success == status else {
throw DatabaseError.preparedStatementFailedToInitialize(reason: preparedStatementError())
}
}
deinit {
duckdb_destroy_prepare(ptr)
ptr.deallocate()
}
/// Executes the prepared statement
///
/// Issues the parameterized query to the database using the values previously
/// bound via the `bind(_:at:)` set of functions.
///
/// - Throws: ``DatabaseError/preparedStatementQueryError(reason:)``
public func execute() throws -> ResultSet {
try ResultSet(prepared: self)
}
func parameterType(at index: Int) -> DatabaseType {
let cparamtype = duckdb_param_type(ptr.pointee, DBInt(index))
return cparamtype.asTypeID
}
func clearBindings() {
duckdb_clear_bindings(ptr.pointee)
}
func withCPreparedStatement<T>(_ body: (duckdb_prepared_statement?) throws -> T) rethrows -> T {
try body(ptr.pointee)
}
}
public extension PreparedStatement {
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Bool?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_boolean(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Int8?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_int8(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Int16?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_int16(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Int32?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_int32(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Int64?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_int64(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: IntHuge?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_hugeint(ptr.pointee, .init(index), .init(value)) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: UIntHuge?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_uhugeint(ptr.pointee, .init(index), .init(value)) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Decimal?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_decimal(ptr.pointee, .init(index), try .init(value)) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: UInt8?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_uint8(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: UInt16?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_uint16(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: UInt32?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_uint32(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: UInt64?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_uint64(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Float?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_float(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Double?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_double(ptr.pointee, .init(index), value) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Date?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_date(ptr.pointee, .init(index), .init(date: value)) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Time?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand { duckdb_bind_time(ptr.pointee, .init(index), .init(time: value)) }
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Timestamp?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand {
duckdb_bind_timestamp(ptr.pointee, .init(index), .init(timestamp: value))
}
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Interval?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand {
duckdb_bind_interval(ptr.pointee, .init(index), .init(interval: value))
}
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: String?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
let data = value.data(using: .utf8)!
try withThrowingCommand {
data.withUnsafeBytes { dataPtr in
duckdb_bind_varchar_length(
ptr.pointee, .init(index), dataPtr.baseAddress, .init(dataPtr.count))
}
}
}
/// Binds a value of the given type at the specified parameter index
///
/// Sets the value that will be used for the next call to ``execute()``.
///
/// - Important: Prepared statement parameters use one-based indexing
/// - Parameter value: the value to bind
/// - Parameter index: the one-based parameter index
/// - Throws: ``DatabaseError/preparedStatementFailedToBindParameter(reason:)``
/// if there is a type-mismatch between the value being bound and the
/// underlying column type
func bind(_ value: Data?, at index: Int) throws {
guard let value = try unwrapValueOrBindNull(value, at: index) else { return }
try withThrowingCommand {
value.withUnsafeBytes { dataPtr in
duckdb_bind_blob(
ptr.pointee, .init(index), dataPtr.baseAddress, .init(dataPtr.count))
}
}
}
}
private extension PreparedStatement {
func bindNullValue(at index: Int) throws {
try withThrowingCommand { duckdb_bind_null(ptr.pointee, .init(index)) }
}
func unwrapValueOrBindNull<T>(_ value: T?, at index: Int) throws -> T? {
guard let value else {
try bindNullValue(at: index)
return nil
}
return value
}
func withThrowingCommand(_ body: () throws -> duckdb_state) throws {
let state = try body()
guard state == .success else {
throw DatabaseError.preparedStatementFailedToBindParameter(reason: preparedStatementError())
}
}
func preparedStatementError() -> String? {
duckdb_prepare_error(ptr.pointee).map(String.init(cString:))
}
}

View File

@@ -0,0 +1,251 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
import Foundation
/// An object representing a DuckDB result set
///
/// A DuckDB result set contains the data returned from the database after a
/// successful query.
///
/// A result set is organized into vertical table slices called columns. Each
/// column of the result set is accessible by calling the ``subscript(_:)``
/// method of the result.
///
/// Elements of a column can be accessed by casting the column to the native
/// Swift type that matches the underlying database column type. See ``Column``
/// for further discussion.
public struct ResultSet: Sendable {
/// The number of chunks in the result set
public var chunkCount: DBInt { chunkStorage.chunkCount }
/// The number of columns in the result set
public var columnCount: DBInt { storage.columnCount }
/// The total number of rows in the result set
public var rowCount: DBInt { chunkStorage.totalRowCount }
private let storage: ResultStorage
private let chunkStorage: ChunkStorage
init(connection: Connection, sql: String) throws {
self.storage = try ResultStorage(connection: connection, sql: sql)
self.chunkStorage = ChunkStorage(resultStorage: storage)
}
init(prepared: PreparedStatement) throws {
self.storage = try ResultStorage(prepared: prepared)
self.chunkStorage = ChunkStorage(resultStorage: storage)
}
/// Returns a `Void` typed column for the given column index
///
/// A `Void` typed column can be cast to a column matching the underlying
/// database representation using ``Column/cast(to:)-4376d``. See ``Column``
/// for further discussion.
///
/// - Parameter columnIndex: the index of the column in the result set
/// - Returns: a `Void` typed column
public func column(at columnIndex: DBInt) -> Column<Void> {
precondition(columnIndex < columnCount)
return Column(result: self, columnIndex: columnIndex) { $0.unwrapNull() ? nil : () }
}
/// The underlying column name for the given column index
///
/// - Parameter columnIndex: the index of the column in the result set
/// - Returns: the name of the column
public func columnName(at columnIndex: DBInt) -> String {
storage.columnName(at: columnIndex)
}
/// The index of the given column name
///
/// - Parameter columnName: the name of the column in the result set
/// - Returns: the index of the column
/// - Complexity: O(n)
public func index(forColumnName columnName: String) -> DBInt? {
for i in 0..<columnCount {
if self.columnName(at: i) == columnName {
return i
}
}
return nil
}
func columnDataType(at index: DBInt) -> DatabaseType {
storage.columnDataType(at: index)
}
func columnLogicalType(at index: DBInt) -> LogicalType {
storage.columnLogicalType(at: index)
}
func element(forColumn columnIndex: DBInt, at index: DBInt) -> Vector.Element {
var chunkIndex = DBInt.zero
var chunkRowOffset = DBInt.zero
while chunkIndex < chunkCount {
let chunk = chunkStorage[Int(chunkIndex)]
let chunkCount = chunk.count
if index < chunkRowOffset + chunkCount {
return chunk.withVector(at: columnIndex) { vector in
vector[Int(index - chunkRowOffset)]
}
}
else {
chunkIndex += 1
chunkRowOffset += chunkCount
}
}
preconditionFailure("item out of bounds")
}
}
// MARK: - Collection conformance
extension ResultSet: RandomAccessCollection {
public typealias Element = Column<Void>
public struct Iterator: IteratorProtocol {
private let result: ResultSet
private var position: DBInt
init(result: ResultSet) {
self.result = result
self.position = result.startIndex
}
public mutating func next() -> Element? {
guard position < result.endIndex else { return nil }
defer { position += 1 }
return .some(result[position])
}
}
public var startIndex: DBInt { 0 }
public var endIndex: DBInt { columnCount }
public subscript(position: DBInt) -> Column<Void> {
column(at: position)
}
public func makeIterator() -> Iterator {
Iterator(result: self)
}
public func index(after i: DBInt) -> DBInt { i + 1 }
public func index(before i: DBInt) -> DBInt { i - 1 }
}
// MARK: - Debug Description
extension ResultSet: CustomDebugStringConvertible {
public var debugDescription: String {
let summary = "chunks: \(chunkCount); rows: \(rowCount); columns: \(columnCount); layout:"
var columns = [String]()
for i in 0..<columnCount {
let name = columnName(at: i)
let type = columnDataType(at: i).description.uppercased()
columns.append("\t\(name) \(type)")
}
return "<\(Self.self): { \(summary) (\n\(columns.joined(separator: ",\n"))\n);>"
}
}
// MARK: - Utilities
fileprivate final class ResultStorage: Sendable {
let columnCount: DBInt
private let ptr = UnsafeMutablePointer<duckdb_result>.allocate(capacity: 1)
init(connection: Connection, sql: String) throws {
let status = sql.withCString { [ptr] queryStrPtr in
connection.withCConnection { duckdb_query($0, queryStrPtr, ptr) }
}
guard status == .success else {
let error = duckdb_result_error(ptr).map(String.init(cString:))
throw DatabaseError.connectionQueryError(reason: error)
}
self.columnCount = duckdb_column_count(ptr)
}
init(prepared: PreparedStatement) throws {
let status = prepared.withCPreparedStatement { [ptr] in duckdb_execute_prepared($0, ptr) }
guard status == .success else {
let error = duckdb_result_error(ptr).map(String.init(cString:))
throw DatabaseError.preparedStatementQueryError(reason: error)
}
self.columnCount = duckdb_column_count(ptr)
}
func columnName(at columnIndex: DBInt) -> String {
String(cString: duckdb_column_name(ptr, columnIndex))
}
func columnDataType(at index: DBInt) -> DatabaseType {
let dataType = duckdb_column_type(ptr, index)
return DatabaseType(rawValue: dataType.rawValue)
}
func columnLogicalType(at index: DBInt) -> LogicalType {
return LogicalType { duckdb_column_logical_type(ptr, index) }
}
func withCResult<T>(_ body: (duckdb_result) throws -> T) rethrows -> T {
try body(ptr.pointee)
}
deinit {
duckdb_destroy_result(ptr)
ptr.deallocate()
}
}
fileprivate final class ChunkStorage: Sendable {
let chunkCount: DBInt
let totalRowCount: DBInt
private let chunks: [DataChunk]
init(resultStorage: ResultStorage) {
let chunkCount = resultStorage.withCResult { duckdb_result_chunk_count($0) }
var chunks = [DataChunk]()
var totalRowCount = DBInt(0)
for i in 0 ..< chunkCount {
let chunk = resultStorage.withCResult { DataChunk(cresult: $0, index: i) }
chunks.append(chunk)
totalRowCount += chunk.count
}
self.chunks = chunks
self.chunkCount = chunkCount
self.totalRowCount = totalRowCount
}
subscript(position: Int) -> DataChunk { chunks[position] }
}

View File

@@ -0,0 +1,69 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
/// A date in the Gregorian calendar
///
/// A date specifies a combination of year, month and day. DuckDB follows the
/// SQL standards lead by counting dates exclusively in the Gregorian calendar,
/// even for years before that calendar was in use.
public struct Date: Hashable, Equatable, Codable, Sendable {
/// days since the unix date epoch `1970-01-01`
public var days: Int32
}
public extension Date {
/// The components of ``Date`` decomposed into its constituent parts
///
/// A type to facilate the conversion between nominal units of years, months
/// and days into the underlying DuckDB ``Date`` representation of days since
/// `1970-01-01`.
struct Components: Hashable, Equatable {
public var year: Int32
public var month: Int8
public var day: Int8
}
/// Creates a new instance from the given date components
///
/// - Parameter components: the date components of the instance
init(components: Components) {
let cdatestruct = duckdb_to_date(duckdb_date_struct(components: components))
self = cdatestruct.asDate
}
/// Date components
var components: Components { Components(self) }
}
private extension Date.Components {
init(_ date: Date) {
let cdate = duckdb_date(date: date)
let cdatestruct = duckdb_from_date(cdate)
self = cdatestruct.asDateComponents
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
/// A period of time
///
/// Intervals represent a period of time. This period can be measured in a
/// variety of units, for example years, days, or seconds.
public struct Interval: Hashable, Equatable, Sendable {
public var months: Int32
public var days: Int32
public var microseconds: Int64
}
extension Interval {
init(
years: Int32,
months: Int32,
days: Int32,
hours: Int32,
minutes: Int32,
seconds: Int32,
microseconds: Int64
) {
let hours_ms = Int64(hours) * 60 * 60 * 1_000_000
let minutes_ms = Int64(minutes) * 60 * 1_000_000
let seconds_ms = Int64(seconds) * 1_000_000
self.init(
months: (years * 12) + months,
days: days,
microseconds: hours_ms + minutes_ms + seconds_ms + microseconds
)
}
}

View File

@@ -0,0 +1,76 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
/// A point in absolute time
///
/// Time represents points in absolute time, usually called instants.
/// DuckDB represents instants as the number of microseconds (µs) since
/// `1970-01-01 00:00:00+00`.
public struct Time: Hashable, Equatable, Sendable {
/// microseconds (µs) since `1970-01-01 00:00:00+00`.
public var microseconds: Int64
}
public extension Time {
/// The components of ``Time`` decomposed into its constituent parts
///
/// A type to facilate the conversion between nominal units of hours,
/// minutes, seconds and microseconds into the underlying DuckDB
/// ``Time`` representation of microseconds (µs) since
/// `1970-01-01 00:00:00+00`.
struct Components: Hashable, Equatable {
public var hour: Int8
public var minute: Int8
public var second: Int8
public var microsecond: Int32
}
/// Creates a new instance from the given time components
///
/// - Parameter components: the time components of the instance
init(components: Components) {
let ctimestruct = duckdb_to_time(duckdb_time_struct(components: components))
self = ctimestruct.asTime
}
/// Time components
var components: Components { Components(self) }
}
private extension Time.Components {
init(_ time: Time) {
let ctime = duckdb_time(time: time)
let ctimestruct = duckdb_from_time(ctime)
self = ctimestruct.asTimeComponents
}
}
public struct TimeTz: Hashable, Equatable, Sendable {
public var time: Time
public var offset: Int32
}

View File

@@ -0,0 +1,87 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
@_implementationOnly import Cduckdb
/// A point in absolute time
///
/// Timestamps represent points in absolute time, usually called instants.
/// DuckDB represents instants as the number of microseconds (µs) since
/// `1970-01-01 00:00:00+00`.
public struct Timestamp: Hashable, Equatable, Codable, Sendable {
/// microseconds (µs) since `1970-01-01 00:00:00+00`.
public var microseconds: Int64
}
public extension Timestamp {
/// The components of a ``Timestamp`` decomposed into its constituent parts
///
/// A type to facilate the conversion between nominal units of year, month,
/// day, hours, minutes, seconds and microseconds into the underlying DuckDB
/// ``Timestamp`` representation of microseconds (µs) since
/// `1970-01-01 00:00:00+00`.
struct Components: Hashable, Equatable {
/// Date components
public var date: Date.Components
/// Time components
public var time: Time.Components
}
/// Creates a new instance from the given timestamp components
///
/// - Parameter components: the components of the timestamp to be instantiated
init(components: Components) {
let ctimestampstruct = duckdb_to_timestamp(duckdb_timestamp_struct(components: components))
self = ctimestampstruct.asTimestamp
}
/// Timestamp components
var components: Components { Components(self) }
}
private extension Timestamp.Components {
init(_ timestamp: Timestamp) {
let ctimestamp = duckdb_timestamp(timestamp: timestamp)
let ctimestampstruct = duckdb_from_timestamp(ctimestamp)
self = ctimestampstruct.asTimestampComponents
}
}
extension Timestamp.Components {
init(
year: Int32,
month: Int8,
day: Int8,
hour: Int8,
minute: Int8,
second: Int8,
microsecond: Int32
) {
self.date = .init(year: year, month: month, day: day)
self.time = .init(hour: hour, minute: minute, second: second, microsecond: microsecond)
}
}

View File

@@ -0,0 +1,242 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
import XCTest
@testable import DuckDB
final class AppenderTests: XCTestCase {
func test_bool_round_trip() throws {
try roundTripTest(
dataType: "BOOL",
expected: [false, true, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Bool.self) }
)
}
func test_utinyint_round_trip() throws {
try roundTripTest(
dataType: "UTINYINT",
expected: [UInt8.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: UInt8.self) }
)
}
func test_usmallint_round_trip() throws {
try roundTripTest(
dataType: "USMALLINT",
expected: [UInt16.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: UInt16.self) }
)
}
func test_uint_round_trip() throws {
try roundTripTest(
dataType: "UINTEGER",
expected: [UInt32.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: UInt32.self) }
)
}
func test_ubigint_round_trip() throws {
try roundTripTest(
dataType: "UBIGINT",
expected: [UInt64.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: UInt64.self) }
)
}
func test_tinyint_round_trip() throws {
try roundTripTest(
dataType: "TINYINT",
expected: [Int8.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Int8.self) }
)
}
func test_smallint_round_trip() throws {
try roundTripTest(
dataType: "SMALLINT",
expected: [Int16.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Int16.self) }
)
}
func test_int_round_trip() throws {
try roundTripTest(
dataType: "INTEGER",
expected: [Int32.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Int32.self) }
)
}
func test_bigint_round_trip() throws {
try roundTripTest(
dataType: "BIGINT",
expected: [Int64.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Int64.self) }
)
}
func test_hugeint_round_trip() throws {
try roundTripTest(
dataType: "HUGEINT",
expected: [IntHuge.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: IntHuge.self) }
)
}
func test_uhugeint_round_trip() throws {
try roundTripTest(
dataType: "UHUGEINT",
expected: [UIntHuge.min, .max, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: UIntHuge.self) }
)
}
func test_float_round_trip() throws {
try roundTripTest(
dataType: "FLOAT",
expected: [-Float.greatestFiniteMagnitude, .greatestFiniteMagnitude, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Float.self) }
)
}
func test_double_round_trip() throws {
try roundTripTest(
dataType: "DOUBLE",
expected: [-Double.greatestFiniteMagnitude, .greatestFiniteMagnitude, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Double.self) }
)
}
func test_varchar_round_trip() throws {
try roundTripTest(
dataType: "VARCHAR",
expected: ["🦆🦆🦆🦆🦆🦆", "goo\0se", nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: String.self) }
)
}
func test_time_round_trip() throws {
let t1 = Time.Components(hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Time.Components(hour: 23, minute: 59, second: 59, microsecond: 999_999)
try roundTripTest(
dataType: "TIME",
expected: [Time(components: t1), Time(components: t2), nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Time.self) }
)
}
func test_date_round_trip() throws {
let d1 = Date.Components(year: -5_877_641, month: 06, day: 25)
let d2 = Date.Components(year: 5_881_580, month: 07, day: 10)
try roundTripTest(
dataType: "DATE",
expected: [Date(components: d1), Date(components: d2), nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Date.self) }
)
}
func test_timestamp_round_trip() throws {
let t1 = Timestamp.Components(
year: -290_308, month: 12, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 294_247, month: 01, day: 10, hour: 04, minute: 0, second: 54, microsecond: 775_806)
try roundTripTest(
dataType: "TIMESTAMP",
expected: [Timestamp(components: t1), Timestamp(components: t2), nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Timestamp.self) }
)
}
func test_interval_round_trip() throws {
let t1 = Interval(
years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, microseconds: 0)
let t2 = Interval(
years: 83, months: 3, days: 999, hours: 0, minutes: 16, seconds: 39, microseconds: 999_999)
try roundTripTest(
dataType: "INTERVAL",
expected: [t1, t2, nil],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Interval.self) }
)
}
func test_blob_round_trip() throws {
try roundTripTest(
dataType: "BLOB",
expected: [
"thisisalongblob\0withnullbytes".data(using: .ascii)!,
"\0\0\0a".data(using: .ascii)!,
nil
],
append: { appender, item in try appender.append(item) },
cast: { $0.cast(to: Data.self) }
)
}
}
private extension AppenderTests {
func roundTripTest<T: Equatable>(
dataType: String,
expected: [T?],
append: (Appender, T?) throws -> Void,
cast: (Column<Void>) -> Column<T>
) throws {
let connection = try Database(store: .inMemory).connect()
try connection.execute("CREATE TABLE t1(i \(dataType));")
let appender = try Appender(connection: connection, table: "t1")
for item in expected {
try append(appender, item)
try appender.endRow()
}
try appender.flush()
let result = try connection.query("SELECT * FROM t1;")
let column = cast(result[0])
for (index, item) in expected.enumerated() {
XCTAssertEqual(column[DBInt(index)], item)
}
}
}

View File

@@ -0,0 +1,47 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import XCTest
@testable import DuckDB
final class CodingUserInfoKeysTests: XCTestCase {
func test_logical_type() throws {
let connection = try Database(store: .inMemory).connect()
try connection.execute("""
CREATE TABLE t1(int_list INT[]);
INSERT INTO t1 VALUES ([1, 2, 3]);
""")
let result = try connection.query("SELECT * FROM t1;")
let column = result[0].cast(to: TestDynamicDecodable.self)
XCTAssertEqual(column[0]?.logicalType?.dataType, .list)
}
}
struct TestDynamicDecodable: Decodable {
let logicalType: LogicalType?
init(from decoder: Decoder) throws {
self.logicalType = decoder.userInfo[CodingUserInfoKeys.logicalTypeCodingUserInfoKey] as? LogicalType
}
}

View File

@@ -0,0 +1,85 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import XCTest
@testable import DuckDB
final class DatabaseTests: XCTestCase {
func test_create_in_memory_database() throws {
let _ = try Database(store: .inMemory)
}
func test_create_local_file_datebase() throws {
let fileURL = try Self.generateTemporaryFileURL(forFileNamed: "test.tb")
defer { Self.cleanUpTemporaryFileURL(fileURL) }
let _ = try Database(store: .file(at: fileURL))
}
func test_connect_to_in_memory_datebase() throws {
let _ = try Database(store: .inMemory).connect()
}
func test_connect_to_local_file_datebase() throws {
let fileURL = try Self.generateTemporaryFileURL(forFileNamed: "test.tb")
defer { Self.cleanUpTemporaryFileURL(fileURL) }
let _ = try Database(store: .file(at: fileURL)).connect()
}
func test_execute_statement() throws {
let connection = try Database(store: .inMemory).connect()
try connection.execute("SELECT version();")
}
func test_query_result() throws {
let connection = try Database(store: .inMemory).connect()
let result = try connection.query("SELECT * FROM test_all_types();")
let element: Void? = result[0][0]
XCTAssertNotNil(element)
}
}
// MARK: - Temp Directory Helpers
private extension DatabaseTests {
static func generateTemporaryFileURL(forFileNamed fileName: String) throws -> URL {
let tmpDirURL = try FileManager.default.url(
for: .itemReplacementDirectory,
in: .userDomainMask,
appropriateFor: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0],
create: true
)
return tmpDirURL.appendingPathComponent(fileName)
}
static func cleanUpTemporaryFileURL(_ fileURL: URL) {
do {
try FileManager.default.removeItem(at: fileURL.deletingLastPathComponent())
}
catch {
print("Ignored failed attempt to remove temp dir at \(fileURL):\n\t\(error)")
}
}
}

View File

@@ -0,0 +1,52 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
import XCTest
@testable import DuckDB
final class DecimalUtilityTests: XCTestCase {
func test_decimal_int_huge_min() throws {
let minInt128 = Decimal(string: "\(IntHuge.min)")!
XCTAssertEqual(Decimal(IntHuge.min), minInt128)
}
func test_decimal_int_huge_max() throws {
let maxInt128 = Decimal(string: "\(IntHuge.max)")!
XCTAssertEqual(Decimal(IntHuge.max), maxInt128)
}
func test_decimal_uint_huge_min() throws {
let minUInt128 = Decimal(string: "\(UIntHuge.min)")!
XCTAssertEqual(Decimal(UIntHuge.min), minUInt128)
}
func test_decimal_uint_huge_max() throws {
let maxUInt128 = Decimal(string: "\(UIntHuge.max)")!
XCTAssertEqual(Decimal(UIntHuge.max), maxUInt128)
}
}

View File

@@ -0,0 +1,60 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
import XCTest
@testable import DuckDB
final class ExtensionTests: XCTestCase {
func test_parquet() throws {
try loadExtension(extension_name: "parquet")
}
func test_json() throws {
try loadExtension(extension_name: "json")
}
func test_icu() throws {
try loadExtension(extension_name: "icu")
}
}
private extension ExtensionTests {
func loadExtension(extension_name: String) throws {
let connection = try Database(store: .inMemory).connect()
try connection.execute("LOAD \(extension_name)")
let prepped = try PreparedStatement(
connection: connection,
query: "SELECT installed, loaded FROM duckdb_extensions() where extension_name = $1"
)
try prepped.bind(extension_name, at: 1);
let result = try prepped.execute()
XCTAssertEqual(result[0].cast(to: Bool.self)[0], true)
XCTAssertEqual(result[1].cast(to: Bool.self)[0], true)
}
}

View File

@@ -0,0 +1,405 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
import XCTest
@testable import DuckDB
final class LogicalTypeTests: XCTestCase {
func test_boolean() throws {
try logicalTypeTest(
dataType: "BOOL",
cast: { $0.cast(to: Bool.self) },
validate: {
XCTAssertEqual($0.dataType, .boolean)
XCTAssertEqual($0.underlyingDataType, .boolean)
}
)
}
func test_tinyint() throws {
try logicalTypeTest(
dataType: "TINYINT",
cast: { $0.cast(to: Int8.self) },
validate: {
XCTAssertEqual($0.dataType, .tinyint)
XCTAssertEqual($0.underlyingDataType, .tinyint)
}
)
}
func test_smallint() throws {
try logicalTypeTest(
dataType: "SMALLINT",
cast: { $0.cast(to: Int16.self) },
validate: {
XCTAssertEqual($0.dataType, .smallint)
XCTAssertEqual($0.underlyingDataType, .smallint)
}
)
}
func test_integer() throws {
try logicalTypeTest(
dataType: "INTEGER",
cast: { $0.cast(to: Int32.self) },
validate: {
XCTAssertEqual($0.dataType, .integer)
XCTAssertEqual($0.underlyingDataType, .integer)
}
)
}
func test_bigint() throws {
try logicalTypeTest(
dataType: "BIGINT",
cast: { $0.cast(to: Int64.self) },
validate: {
XCTAssertEqual($0.dataType, .bigint)
XCTAssertEqual($0.underlyingDataType, .bigint)
}
)
}
func test_hugeint() throws {
try logicalTypeTest(
dataType: "HUGEINT",
cast: { $0.cast(to: IntHuge.self) },
validate: {
XCTAssertEqual($0.dataType, .hugeint)
XCTAssertEqual($0.underlyingDataType, .hugeint)
}
)
}
func test_uhugeint() throws {
try logicalTypeTest(
dataType: "UHUGEINT",
cast: { $0.cast(to: UIntHuge.self) },
validate: {
XCTAssertEqual($0.dataType, .uhugeint)
XCTAssertEqual($0.underlyingDataType, .uhugeint)
}
)
}
func test_utinyint() throws {
try logicalTypeTest(
dataType: "UTINYINT",
cast: { $0.cast(to: UInt8.self) },
validate: {
XCTAssertEqual($0.dataType, .utinyint)
XCTAssertEqual($0.underlyingDataType, .utinyint)
}
)
}
func test_usmallint() throws {
try logicalTypeTest(
dataType: "USMALLINT",
cast: { $0.cast(to: UInt16.self) },
validate: {
XCTAssertEqual($0.dataType, .usmallint)
XCTAssertEqual($0.underlyingDataType, .usmallint)
}
)
}
func test_uinteger() throws {
try logicalTypeTest(
dataType: "UINTEGER",
cast: { $0.cast(to: UInt32.self) },
validate: {
XCTAssertEqual($0.dataType, .uinteger)
XCTAssertEqual($0.underlyingDataType, .uinteger)
}
)
}
func test_ubigint() throws {
try logicalTypeTest(
dataType: "UBIGINT",
cast: { $0.cast(to: UInt64.self) },
validate: {
XCTAssertEqual($0.dataType, .ubigint)
XCTAssertEqual($0.underlyingDataType, .ubigint)
}
)
}
func test_float() throws {
try logicalTypeTest(
dataType: "FLOAT",
cast: { $0.cast(to: Float.self) },
validate: {
XCTAssertEqual($0.dataType, .float)
XCTAssertEqual($0.underlyingDataType, .float)
}
)
}
func test_double() throws {
try logicalTypeTest(
dataType: "DOUBLE",
cast: { $0.cast(to: Double.self) },
validate: {
XCTAssertEqual($0.dataType, .double)
XCTAssertEqual($0.underlyingDataType, .double)
}
)
}
func test_timestamp() throws {
try logicalTypeTest(
dataType: "TIMESTAMP",
cast: { $0.cast(to: Timestamp.self) },
validate: {
XCTAssertEqual($0.dataType, .timestamp)
XCTAssertEqual($0.underlyingDataType, .timestamp)
}
)
}
func test_timestamp_s() throws {
try logicalTypeTest(
dataType: "TIMESTAMP_S",
cast: { $0.cast(to: Timestamp.self) },
validate: {
XCTAssertEqual($0.dataType, .timestampS)
XCTAssertEqual($0.underlyingDataType, .timestampS)
}
)
}
func test_timestamp_ms() throws {
try logicalTypeTest(
dataType: "TIMESTAMP_MS",
cast: { $0.cast(to: Timestamp.self) },
validate: {
XCTAssertEqual($0.dataType, .timestampMS)
XCTAssertEqual($0.underlyingDataType, .timestampMS)
}
)
}
func test_timestamp_ns() throws {
try logicalTypeTest(
dataType: "TIMESTAMP_NS",
cast: { $0.cast(to: Timestamp.self) },
validate: {
XCTAssertEqual($0.dataType, .timestampNS)
XCTAssertEqual($0.underlyingDataType, .timestampNS)
}
)
}
func test_date() throws {
try logicalTypeTest(
dataType: "DATE",
cast: { $0.cast(to: Date.self) },
validate: {
XCTAssertEqual($0.dataType, .date)
XCTAssertEqual($0.underlyingDataType, .date)
}
)
}
func test_time() throws {
try logicalTypeTest(
dataType: "TIME",
cast: { $0.cast(to: Time.self) },
validate: {
XCTAssertEqual($0.dataType, .time)
XCTAssertEqual($0.underlyingDataType, .time)
}
)
}
func test_interval() throws {
try logicalTypeTest(
dataType: "INTERVAL",
cast: { $0.cast(to: Interval.self) },
validate: {
XCTAssertEqual($0.dataType, .interval)
XCTAssertEqual($0.underlyingDataType, .interval)
}
)
}
func test_varchar() throws {
try logicalTypeTest(
dataType: "VARCHAR",
cast: { $0.cast(to: String.self) },
validate: {
XCTAssertEqual($0.dataType, .varchar)
XCTAssertEqual($0.underlyingDataType, .varchar)
}
)
}
func test_blob() throws {
try logicalTypeTest(
dataType: "BLOB",
cast: { $0.cast(to: Data.self) },
validate: {
XCTAssertEqual($0.dataType, .blob)
XCTAssertEqual($0.underlyingDataType, .blob)
}
)
}
func test_decimal() throws {
try logicalTypeTest(
dataType: "DECIMAL(38, 10)",
cast: { $0.cast(to: Decimal.self) },
validate: {
XCTAssertEqual($0.dataType, .decimal)
XCTAssertEqual($0.underlyingDataType, .decimal)
let properties = $0.decimalProperties!
XCTAssertEqual(properties.scale, 10)
XCTAssertEqual(properties.width, 38)
XCTAssertEqual(properties.storageType, .hugeint)
}
)
}
enum Mood: String, Equatable, Decodable {
case sad
case ok
case happy
}
func test_enum() throws {
try logicalTypeTest(
dataType: "mood",
cast: { $0.cast(to: Mood.self) },
before: { connection in
try connection.execute("CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');")
},
validate: {
XCTAssertEqual($0.dataType, .enum)
XCTAssertEqual($0.underlyingDataType, .utinyint)
}
)
}
func test_list() throws {
try logicalTypeTest(
dataType: "INT[]",
cast: { $0.cast(to: [Int32?].self) },
validate: {
XCTAssertEqual($0.dataType, .list)
XCTAssertEqual($0.underlyingDataType, .list)
XCTAssertEqual($0.listChildType?.dataType, .integer)
}
)
}
struct NumStrStruct: Equatable, Decodable {
let num: Int32
let str: String
}
func test_struct() throws {
let connection = try Database(store: .inMemory).connect()
try connection.execute("CREATE TABLE t1(num INTEGER, str VARCHAR);")
let result = try connection.query("SELECT {'num': num, 'str': str} as struct_column FROM t1;")
let logicalType = result[0].cast(to: NumStrStruct.self).underlyingLogicalType
XCTAssertEqual(logicalType.dataType, .struct)
XCTAssertEqual(logicalType.underlyingDataType, .struct)
let properties = logicalType.structMemberProperties!
XCTAssertEqual(properties.count, 2)
XCTAssertEqual(properties[0].name, "num")
XCTAssertEqual(properties[0].type.dataType, .integer)
XCTAssertEqual(properties[1].name, "str")
XCTAssertEqual(properties[1].type.dataType, .varchar)
}
func test_map() throws {
try logicalTypeTest(
dataType: "MAP(INT,DOUBLE)",
cast: { $0.cast(to: [Int32: Double].self) },
validate: {
XCTAssertEqual($0.dataType, .map)
XCTAssertEqual($0.underlyingDataType, .map)
XCTAssertEqual($0.mapKeyType?.dataType, .integer)
XCTAssertEqual($0.mapValueType?.dataType, .double)
}
)
}
enum NumStrUnion: Equatable, Decodable {
case num(Int32)
case str(String)
}
func test_union() throws {
try logicalTypeTest(
dataType: "UNION(num INT, str VARCHAR)",
cast: { $0.cast(to: NumStrUnion.self) },
validate: {
XCTAssertEqual($0.dataType, .union)
XCTAssertEqual($0.underlyingDataType, .union)
let properties = $0.unionMemberProperties!
XCTAssertEqual(properties.count, 2)
XCTAssertEqual(properties[0].name, "num")
XCTAssertEqual(properties[0].type.dataType, .integer)
XCTAssertEqual(properties[1].name, "str")
XCTAssertEqual(properties[1].type.dataType, .varchar)
}
)
}
func test_uuid() throws {
try logicalTypeTest(
dataType: "UUID",
cast: { $0.cast(to: UUID.self) },
validate: {
XCTAssertEqual($0.dataType, .uuid)
XCTAssertEqual($0.underlyingDataType, .uuid)
}
)
}
}
private extension LogicalTypeTests {
func logicalTypeTest<T: Equatable>(
dataType: String,
cast: (Column<Void>) -> Column<T>,
before: ((Connection) throws -> Void)? = nil,
validate: (LogicalType) -> Void
) throws {
let connection = try Database(store: .inMemory).connect()
try before?(connection)
try connection.execute("CREATE TABLE t1(i \(dataType));")
let result = try connection.query("SELECT * FROM t1;")
validate(cast(result[0]).underlyingLogicalType)
}
}

View File

@@ -0,0 +1,294 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
import XCTest
@testable import DuckDB
final class PreparedStatementTests: XCTestCase {
func test_bool_round_trip() throws {
try roundTripTest(
dataType: "BOOL",
expected: [false, true, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Bool.self) }
)
}
func test_utinyint_round_trip() throws {
try roundTripTest(
dataType: "UTINYINT",
expected: [UInt8.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: UInt8.self) }
)
}
func test_usmallint_round_trip() throws {
try roundTripTest(
dataType: "USMALLINT",
expected: [UInt16.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: UInt16.self) }
)
}
func test_uint_round_trip() throws {
try roundTripTest(
dataType: "UINTEGER",
expected: [UInt32.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: UInt32.self) }
)
}
func test_ubigint_round_trip() throws {
try roundTripTest(
dataType: "UBIGINT",
expected: [UInt64.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: UInt64.self) }
)
}
func test_tinyint_round_trip() throws {
try roundTripTest(
dataType: "TINYINT",
expected: [Int8.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Int8.self) }
)
}
func test_smallint_round_trip() throws {
try roundTripTest(
dataType: "SMALLINT",
expected: [Int16.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Int16.self) }
)
}
func test_int_round_trip() throws {
try roundTripTest(
dataType: "INTEGER",
expected: [Int32.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Int32.self) }
)
}
func test_bigint_round_trip() throws {
try roundTripTest(
dataType: "BIGINT",
expected: [Int64.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Int64.self) }
)
}
func test_hugeint_round_trip() throws {
try roundTripTest(
dataType: "HUGEINT",
expected: [IntHuge.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: IntHuge.self) }
)
}
func test_uhugeint_round_trip() throws {
try roundTripTest(
dataType: "UHUGEINT",
expected: [UIntHuge.min, .max, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: UIntHuge.self) }
)
}
func test_float_round_trip() throws {
try roundTripTest(
dataType: "FLOAT",
expected: [-Float.greatestFiniteMagnitude, .greatestFiniteMagnitude, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Float.self) }
)
}
func test_double_round_trip() throws {
try roundTripTest(
dataType: "DOUBLE",
expected: [-Double.greatestFiniteMagnitude, .greatestFiniteMagnitude, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Double.self) }
)
}
func test_varchar_round_trip() throws {
try roundTripTest(
dataType: "VARCHAR",
expected: ["🦆🦆🦆🦆🦆🦆", "goo\0se", nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: String.self) }
)
}
func test_time_round_trip() throws {
let t1 = Time.Components(hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Time.Components(hour: 23, minute: 59, second: 59, microsecond: 999_999)
try roundTripTest(
dataType: "TIME",
expected: [Time(components: t1), Time(components: t2), nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Time.self) }
)
}
func test_date_round_trip() throws {
let d1 = Date.Components(year: -5_877_641, month: 06, day: 25)
let d2 = Date.Components(year: 5_881_580, month: 07, day: 10)
try roundTripTest(
dataType: "DATE",
expected: [Date(components: d1), Date(components: d2), nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Date.self) }
)
}
func test_timestamp_round_trip() throws {
let t1 = Timestamp.Components(
year: -290_308, month: 12, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 294_247, month: 01, day: 10, hour: 04, minute: 0, second: 54, microsecond: 775_806)
try roundTripTest(
dataType: "TIMESTAMP",
expected: [Timestamp(components: t1), Timestamp(components: t2), nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Timestamp.self) }
)
}
func test_interval_round_trip() throws {
let t1 = Interval(
years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, microseconds: 0)
let t2 = Interval(
years: 83, months: 3, days: 999, hours: 0, minutes: 16, seconds: 39, microseconds: 999_999)
try roundTripTest(
dataType: "INTERVAL",
expected: [t1, t2, nil],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Interval.self) }
)
}
func test_blob_round_trip() throws {
try roundTripTest(
dataType: "BLOB",
expected: [
"thisisalongblob\0withnullbytes".data(using: .ascii)!,
"\0\0\0a".data(using: .ascii)!,
nil
],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Data.self) }
)
}
func test_decimal_4_1_roundtrip() throws {
try roundTripTest(
dataType: "DECIMAL(4,1)",
expected: [
Decimal(string: "-999.9"),
Decimal(string: " 999.9"),
nil
],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Decimal.self) }
)
}
func test_decimal_9_4_roundtrip() throws {
try roundTripTest(
dataType: "DECIMAL(9,4)",
expected: [
Decimal(string: "-99999.9999"),
Decimal(string: " 99999.9999"),
nil
],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Decimal.self) }
)
}
func test_decimal_18_6_roundtrip() throws {
try roundTripTest(
dataType: "DECIMAL(18,6)",
expected: [
Decimal(string: "-999999999999.999999"),
Decimal(string: "999999999999.999999"),
nil
],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Decimal.self) }
)
}
func test_decimal_38_10_roundtrip() throws {
try roundTripTest(
dataType: "DECIMAL(38,10)",
expected: [
Decimal(string: "-9999999999999999999999999999.9999999999"),
Decimal(string: "9999999999999999999999999999.9999999999"),
nil
],
bind: { statement, item in try statement.bind(item, at: 1) },
cast: { $0.cast(to: Decimal.self) }
)
}
}
private extension PreparedStatementTests {
func roundTripTest<T: Equatable>(
dataType: String,
expected: [T?],
bind: (PreparedStatement, T?) throws -> Void,
cast: (Column<Void>) -> Column<T>
) throws {
let connection = try Database(store: .inMemory).connect()
try connection.execute("CREATE TABLE t1(i \(dataType));")
let statement = try PreparedStatement(
connection: connection, query: "INSERT INTO t1 VALUES ($1);")
for item in expected {
try bind(statement, item)
let _ = try statement.execute()
}
let result = try connection.query("SELECT * FROM t1;")
let column = cast(result[0])
for (index, item) in expected.enumerated() {
XCTAssertEqual(column[DBInt(index)], item)
}
}
}

View File

@@ -0,0 +1,440 @@
//
// DuckDB
// https://github.com/duckdb/duckdb-swift
//
// Copyright © 2018-2024 Stichting DuckDB Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import Foundation
import XCTest
@testable import DuckDB
final class TypeConversionTests: XCTestCase {
func test_extract_from_bool() throws {
try extractTest(
testColumnName: "bool", expected: [false, true, nil]) { $0.cast(to: Bool.self) }
}
func test_extract_from_utinyint() throws {
try extractTest(
testColumnName: "utinyint", expected: [UInt8.min, .max, nil]) { $0.cast(to: UInt8.self) }
}
func test_extract_from_usmallint() throws {
try extractTest(
testColumnName: "usmallint", expected: [UInt16.min, .max, nil]) { $0.cast(to: UInt16.self) }
}
func test_extract_from_uint() throws {
try extractTest(
testColumnName: "uint", expected: [UInt32.min, .max, nil]) { $0.cast(to: UInt32.self) }
}
func test_extract_from_ubigint() throws {
try extractTest(
testColumnName: "ubigint", expected: [UInt64.min, .max, nil]) { $0.cast(to: UInt64.self) }
}
func test_extract_from_tinyint() throws {
try extractTest(
testColumnName: "tinyint", expected: [Int8.min, .max, nil]) { $0.cast(to: Int8.self) }
}
func test_extract_from_smallint() throws {
try extractTest(
testColumnName: "smallint", expected: [Int16.min, .max, nil]) { $0.cast(to: Int16.self) }
}
func test_extract_from_int() throws {
try extractTest(
testColumnName: "int", expected: [Int32.min, .max, nil]) { $0.cast(to: Int32.self) }
}
func test_extract_from_bigint() throws {
try extractTest(
testColumnName: "bigint", expected: [Int64.min, .max, nil]) { $0.cast(to: Int64.self) }
}
func test_extract_from_hugeint() throws {
let expected = [IntHuge.min, IntHuge.max, nil]
try extractTest(testColumnName: "hugeint", expected: expected) { $0.cast(to: IntHuge.self) }
}
func test_extract_from_uhugeint() throws {
let expected = [UIntHuge.min, UIntHuge.max, nil]
try extractTest(testColumnName: "uhugeint", expected: expected) { $0.cast(to: UIntHuge.self) }
}
func test_extract_from_float() throws {
let expected = [-Float.greatestFiniteMagnitude, .greatestFiniteMagnitude, nil]
try extractTest(testColumnName: "float", expected: expected) { $0.cast(to: Float.self) }
}
func test_extract_from_double() throws {
let expected = [-Double.greatestFiniteMagnitude, .greatestFiniteMagnitude, nil]
try extractTest(testColumnName: "double", expected: expected) { $0.cast(to: Double.self) }
}
func test_extract_from_varchar() throws {
let expected = ["🦆🦆🦆🦆🦆🦆", "goo\0se", nil]
try extractTest(testColumnName: "varchar", expected: expected) { $0.cast(to: String.self) }
}
func test_extract_from_uuid() throws {
let expected = [
UUID(uuidString: "00000000-0000-0000-0000-000000000000"),
UUID(uuidString: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"),
nil
]
try extractTest(testColumnName: "uuid", expected: expected) { $0.cast(to: UUID.self) }
}
func test_extract_from_time() throws {
let expected = [
Time(components: .init(hour: 0, minute: 0, second: 0, microsecond: 0)),
Time(components: .init(hour: 24, minute: 0, second: 0, microsecond: 0)),
nil
]
try extractTest(testColumnName: "time", expected: expected) { $0.cast(to: Time.self) }
}
func test_extract_from_time_tz() throws {
let expected = [
TimeTz(time: Time(components: .init(hour: 0, minute: 0, second: 0, microsecond: 0)), offset: 57599),
TimeTz(time: Time(components: .init(hour: 24, minute: 0, second: 0, microsecond: 0)), offset: -57599),
nil
]
try extractTest(testColumnName: "time_tz", expected: expected) { $0.cast(to: TimeTz.self) }
}
func test_extract_from_date() throws {
let expected = [
Date(components: .init(year: -5_877_641, month: 06, day: 25)),
Date(components: .init(year: 5_881_580, month: 07, day: 10)),
nil
]
try extractTest(testColumnName: "date", expected: expected) { $0.cast(to: Date.self) }
}
func test_extract_from_timestamp() throws {
let t1 = Timestamp.Components(
year: -290_308, month: 12, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 294_247, month: 01, day: 10, hour: 04, minute: 0, second: 54, microsecond: 775_806)
let expected = [Timestamp(components: t1), Timestamp(components: t2), nil]
try extractTest(testColumnName: "timestamp", expected: expected) { $0.cast(to: Timestamp.self) }
}
func test_extract_from_timestamp_tz() throws {
let t1 = Timestamp.Components(
year: -290_308, month: 12, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 294_247, month: 01, day: 10, hour: 04, minute: 0, second: 54, microsecond: 775_806)
let expected = [Timestamp(components: t1), Timestamp(components: t2), nil]
try extractTest(
testColumnName: "timestamp_tz", expected: expected) { $0.cast(to: Timestamp.self) }
}
func test_extract_from_timestamp_s() throws {
let t1 = Timestamp.Components(
year: -290_308, month: 12, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 294_247, month: 01, day: 10, hour: 04, minute: 0, second: 54, microsecond: 0)
let expected = [Timestamp(components: t1), Timestamp(components: t2), nil]
try extractTest(
testColumnName: "timestamp_s", expected: expected) { $0.cast(to: Timestamp.self) }
}
func test_extract_from_timestamp_ms() throws {
let t1 = Timestamp.Components(
year: -290_308, month: 12, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 294_247, month: 01, day: 10, hour: 04, minute: 0, second: 54, microsecond: 775_000)
let expected = [Timestamp(components: t1), Timestamp(components: t2), nil]
try extractTest(
testColumnName: "timestamp_ms", expected: expected) { $0.cast(to: Timestamp.self) }
}
func test_extract_from_timestamp_ns() throws {
let t1 = Timestamp.Components(
year: 1677, month: 09, day: 22, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 2262, month: 04, day: 11, hour: 23, minute: 47, second: 16, microsecond: 854_775)
let expected = [Timestamp(components: t1), Timestamp(components: t2), nil]
try extractTest(
testColumnName: "timestamp_ns", expected: expected) { $0.cast(to: Timestamp.self) }
}
func test_extract_from_interval() throws {
let expected = [
Interval(
years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, microseconds: 0),
Interval(
years: 83, months: 3, days: 999, hours: 0, minutes: 16, seconds: 39, microseconds: 999_999),
nil
]
try extractTest(testColumnName: "interval", expected: expected) { $0.cast(to: Interval.self) }
}
func test_extract_from_blob() throws {
let expected = [
"thisisalongblob\0withnullbytes".data(using: .ascii)!,
"\0\0\0a".data(using: .ascii)!,
nil
]
try extractTest(testColumnName: "blob", expected: expected) { $0.cast(to: Data.self) }
}
func test_extract_from_decimal_4_1() throws {
let expected = [
Decimal(string: "-999.9"),
Decimal(string: " 999.9"),
nil
]
try extractTest(testColumnName: "dec_4_1", expected: expected) { $0.cast(to: Decimal.self) }
}
func test_extract_from_decimal_9_4() throws {
let expected = [
Decimal(string: "-99999.9999"),
Decimal(string: " 99999.9999"),
nil
]
try extractTest(testColumnName: "dec_9_4", expected: expected) { $0.cast(to: Decimal.self) }
}
func test_extract_from_decimal_18_6() throws {
let expected = [
Decimal(string: "-999999999999.999999"),
Decimal(string: "999999999999.999999"),
nil
]
try extractTest(testColumnName: "dec_18_6", expected: expected) { $0.cast(to: Decimal.self) }
}
func test_extract_from_decimal_38_10() throws {
let expected = [
Decimal(string: "-9999999999999999999999999999.9999999999"),
Decimal(string: "9999999999999999999999999999.9999999999"),
nil
]
try extractTest(testColumnName: "dec38_10", expected: expected) { $0.cast(to: Decimal.self) }
}
func test_extract_from_enum_small() throws {
enum SmallEnum: UInt8, RawRepresentable, Decodable {
case duckDuckEnum
case goose
}
let expected = [SmallEnum.duckDuckEnum, .goose, nil]
try extractTest(
testColumnName: "small_enum",
expected: expected,
params: "use_large_enum=true"
) { $0.cast(to: SmallEnum.self) }
}
func test_extract_from_enum_medium() throws {
enum MediumEnum: UInt16, RawRepresentable, Decodable {
case enum0
case enum299 = 299
}
let expected = [MediumEnum.enum0, .enum299, nil]
try extractTest(
testColumnName: "medium_enum",
expected: expected,
params: "use_large_enum=true"
) { $0.cast(to: MediumEnum.self) }
}
func test_extract_from_enum_large() throws {
enum LargeEnum: UInt32, RawRepresentable, Decodable {
case enum0
case enum69_999 = 69_999
}
let expected = [LargeEnum.enum0, .enum69_999, nil]
try extractTest(
testColumnName: "large_enum",
expected: expected,
params: "use_large_enum=true"
) { $0.cast(to: LargeEnum.self) }
}
func test_extract_from_int_array() throws {
let expected = [[], [Int32(42), 999, nil, nil, -42], nil]
try extractTest(testColumnName: "int_array", expected: expected) { $0.cast(to: [Int32?].self) }
}
func test_extract_from_int_array_casting_to_swift_int() throws {
let expected = [[], [Int(42), 999, nil, nil, -42], nil]
try extractTest(testColumnName: "int_array", expected: expected) { $0.cast(to: [Int?].self) }
}
func test_extract_from_double_array() throws {
// We need this contraption to work around .nan != .nan
enum DoubleBox: Equatable {
case normal(Double?)
case nan
init(_ source: Double?) {
switch source {
case let source? where source.isNaN:
self = .nan
case let source:
self = .normal(source)
}
}
}
let source = [[], [Double(42), .nan, .infinity, -.infinity, nil, -42], nil]
let expected = source.map { $0?.map(DoubleBox.init(_:)) }
let connection = try Database(store: .inMemory).connect()
let result = try connection.query("SELECT double_array FROM test_all_types();")
let column = result[0].cast(to: [Double?].self)
for (index, item) in expected.enumerated() {
XCTAssertEqual(column[DBInt(index)]?.map(DoubleBox.init(_:)), item)
}
}
func test_extract_from_date_array() throws {
let expected = [
[],
[
Date(components: .init(year: 1970, month: 01, day: 01)),
Date(days: 2147483647),
Date(days: -2147483647),
nil,
Date(components: .init(year: 2022, month: 05, day: 12)),
],
nil
]
try extractTest(
testColumnName: "date_array", expected: expected
) { $0.cast(to: [DuckDB.Date?].self) }
}
func test_extract_from_timestamptz_array() throws {
let t1 = Timestamp.Components(
year: 1970, month: 01, day: 01, hour: 0, minute: 0, second: 0, microsecond: 0)
let t2 = Timestamp.Components(
year: 2022, month: 05, day: 12, hour: 23, minute: 23, second: 45, microsecond: 0)
let expected = [
[],
[
Timestamp(components: t1),
Timestamp(microseconds: 9223372036854775807),
Timestamp(microseconds: -9223372036854775807),
nil,
Timestamp(components: t2),
],
nil
]
try extractTest(
testColumnName: "timestamptz_array", expected: expected
) { $0.cast(to: [Timestamp?].self) }
}
func test_extract_from_varchar_array() throws {
let expected = [[], ["🦆🦆🦆🦆🦆🦆", "goose", nil, ""], nil]
try extractTest(
testColumnName: "varchar_array", expected: expected
) { $0.cast(to: [String?].self) }
}
func test_extract_from_nested_int_array() throws {
let expected = [
[],
[[], [Int32(42), 999, nil, nil, -42], nil, [], [42, 999, nil, nil, -42]],
nil
]
try extractTest(
testColumnName: "nested_int_array", expected: expected
) { $0.cast(to: [[Int32?]?].self) }
}
func test_extract_from_struct() throws {
struct TestStruct: Decodable, Equatable {
var a: Int32? = nil
var b: String? = nil
}
let expected = [
TestStruct(),
TestStruct(a: 42, b: "🦆🦆🦆🦆🦆🦆"),
nil
]
try extractTest(testColumnName: "struct", expected: expected) { $0.cast(to: TestStruct.self) }
}
func test_extract_from_struct_of_arrays() throws {
struct TestStruct: Decodable, Equatable {
var a: [Int32?]? = nil
var b: [String?]? = nil
}
let expected = [
TestStruct(a: nil, b: nil),
TestStruct(a: [42, 999, nil, nil, -42], b: ["🦆🦆🦆🦆🦆🦆", "goose", nil, ""]),
nil
]
try extractTest(
testColumnName: "struct_of_arrays", expected: expected) { $0.cast(to: TestStruct.self) }
}
func test_extract_from_array_of_structs() throws {
struct TestStruct: Decodable, Equatable {
var a: Int32? = nil
var b: String? = nil
}
let expected = [
[],
[TestStruct(a: nil, b: nil), TestStruct(a: 42, b: "🦆🦆🦆🦆🦆🦆"), nil],
nil
]
try extractTest(
testColumnName: "array_of_structs", expected: expected) { $0.cast(to: [TestStruct?].self) }
}
func test_extract_from_map() throws {
let expected = [
Dictionary(),
Dictionary(uniqueKeysWithValues: [("key1", "🦆🦆🦆🦆🦆🦆"), ("key2", "goose")]),
nil
]
try extractTest(
testColumnName: "map", expected: expected) { $0.cast(to: [String: String].self) }
}
}
private extension TypeConversionTests {
func extractTest<T: Equatable>(
testColumnName: String,
expected: [T?],
params: String = "",
cast: (Column<Void>) -> Column<T>
) throws {
let connection = try Database(store: .inMemory).connect()
let result = try connection.query("SELECT \(testColumnName) FROM test_all_types(\(params));")
let column = cast(result[0])
for (index, item) in expected.enumerated() {
XCTAssertEqual(column[DBInt(index)], item)
}
}
}