Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SQLite Custom Functions are now supported #25

Merged
merged 43 commits into from
Mar 13, 2023

Conversation

danramteke
Copy link
Contributor

@danramteke danramteke commented Jun 26, 2021

This PR adds custom function support to sqlite-nio. Both custom functions and custom aggregates are supported. To find out more about these features, please see the SQLite website about them: https://www.sqlite.org/appfunc.html

There are some queries only possible using custom functions. And further, custom function help soften the need for custom builds of SQLite with various parts of the SQLite amalgamation enabled or disabled: one can simply write a custom function instead.

I have adapted this code from https://github.com/groue/GRDB.swift/blob/master/GRDB/Core/DatabaseFunction.swift by @groue. Because GRDB doesn't support Linux or NIO, I was motivated to adapt his code.

Example:

func testFunctionArgumentString() throws {
	let conn = try SQLiteConnection.open(storage: .memory, threadPool: self.threadPool, on: self.eventLoop).wait()
	defer { try! conn.close().wait() }
	let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
		return values[0].string
	}
	try conn.install(customFunction: fn).wait()
	XCTAssertNil(try conn.query("SELECT f(NULL) as result")
								.map { rows in rows[0].column("result")?.string }.wait())
	XCTAssertEqual("foo", try conn.query("SELECT f('foo') as result")
									.map { rows in rows[0].column("result")?.string }.wait())
}

There are many more examples in the new unit test file.

Lastly, is there a swift-format configuration I can use to format my PR to match the Vapor's code style?

Copy link
Member

@0xTim 0xTim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial scan looks good, I'll do a proper review next week. Note the comment about CI

@@ -10,7 +10,7 @@ defaults:
jobs:
dependents:
runs-on: ubuntu-latest
container: swift:5.2-bionic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to stay as 5.2 to ensure we don't break 5.2 compatibility

Copy link
Contributor Author

@danramteke danramteke Jun 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

I updated the maxim-lobanov/setup-xcode to v1.2.1 since macOS build won't start.

https://github.com/vapor/sqlite-nio/runs/2922618375?check_suite_focus=true

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xTim 5.2 compatibility is checked by the main Linux workflows in each repo (at least, that's how it's supposed to work) - I prefer to run the dependent checks in a "current"-version container to reflect the most common usage, since those checks don't have a version matrix.

@groue
Copy link

groue commented Jun 28, 2021

I have adapted this code from https://github.com/groue/GRDB.swift/blob/master/GRDB/Core/DatabaseFunction.swift by @groue.

Hello, and thank you for getting inspired from my work 👍

GRDB uses the the MIT license. It is a permissive license, which allows you to do anything you like with the licensed code. The only requirement is:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

The SQLiteCustomFunction.swift file in this PR contains substantial portions of https://github.com/groue/GRDB.swift/blob/v5.8.0/GRDB/Core/DatabaseFunction.swift: please include the GRDB copyright and permission notices in your derived work.

@danramteke
Copy link
Contributor Author

Any updates? @gwynne @0xTim

Copy link
Member

@0xTim 0xTim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than the log levels. I'll let @gwynne have a look before merging though

Sources/SQLiteNIO/SQLiteConnection.swift Outdated Show resolved Hide resolved
Sources/SQLiteNIO/SQLiteConnection.swift Outdated Show resolved Hide resolved
@danramteke
Copy link
Contributor Author

Wonderful. I committed both of your suggested changes, @0xTim.

Copy link
Member

@0xTim 0xTim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it's taken so long to get to this! The only thing I think is missing is a NOTICES.txt for attribution to GRDB. IANAL but this is what we've done in other projects, e.g. https://github.com/vapor/jwt-kit/blob/main/NOTICES.txt

Once added I'll merge

@0xTim 0xTim added the semver-minor Contains new APIs label Jun 29, 2022
@Austinpayne
Copy link
Contributor

Austinpayne commented Oct 24, 2022

@danramteke Can I help get this across the finish line? I am interested in this functionality as well. I'd be happy to fill out the NOTICES.txt if you provided me access to your branch. Alternatively, I can create a merge to your original branch.

@danramteke
Copy link
Contributor Author

@Austinpayne had no idea anyone else wanted this, thanks for the nudge!

@danramteke
Copy link
Contributor Author

@0xTim I did my best to add a NOTICES.txt as requested.

@Austinpayne
Copy link
Contributor

Austinpayne commented Oct 26, 2022

@Austinpayne had no idea anyone else wanted this, thanks for the nudge!

Thank you for implementing it! This unlocks some very interesting use cases and I actually had started on an implementation before seeing yours. Your PR is much more feature rich so really looking forward to seeing in mainlined. Thank you @groue as well for the original implementation!

Copy link
Member

@0xTim 0xTim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@0xTim
Copy link
Member

0xTim commented Oct 27, 2022

@gwynne anything I've missed?

@danramteke
Copy link
Contributor Author

@0xTim @gwynne following up after 1 month 👀

@codecov-commenter
Copy link

codecov-commenter commented Dec 4, 2022

Codecov Report

Merging #25 (332a713) into main (6552d66) will increase coverage by 19.75%.
The diff coverage is 66.45%.

Additional details and impacted files
@@             Coverage Diff             @@
##             main      #25       +/-   ##
===========================================
+ Coverage   48.03%   67.78%   +19.75%     
===========================================
  Files           7        8        +1     
  Lines         535      832      +297     
===========================================
+ Hits          257      564      +307     
+ Misses        278      268       -10     
Impacted Files Coverage Δ
Sources/SQLiteNIO/SQLiteError.swift 27.39% <0.00%> (+27.39%) ⬆️
Sources/SQLiteNIO/SQLiteStatement.swift 92.24% <ø> (+12.40%) ⬆️
Sources/SQLiteNIO/SQLiteConnection.swift 64.61% <66.66%> (+3.27%) ⬆️
Sources/SQLiteNIO/SQLiteCustomFunction.swift 84.06% <84.06%> (ø)
Sources/SQLiteNIO/SQLiteData.swift 83.33% <97.36%> (+55.20%) ⬆️

... and 1 file with indirect coverage changes

Copy link
Member

@gwynne gwynne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've commented inline about several minor issues. I'm also very uncomfortable with the amount of manual memory management being thrown around; I'll try to persuade @Lukasa to take a look at what's going on in that regard.

That concern aside, the overall code seems pretty solid and there's no question that having this functionality is both useful and desirable; if we can address the above, this will be good to go!

Sources/SQLiteNIO/SQLiteConnection.swift Outdated Show resolved Hide resolved
Sources/SQLiteNIO/SQLiteConnection.swift Outdated Show resolved Hide resolved
Sources/SQLiteNIO/SQLiteDataConvertible.swift Outdated Show resolved Hide resolved
Sources/SQLiteNIO/SQLiteError.swift Outdated Show resolved Hide resolved
Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift Outdated Show resolved Hide resolved
Sources/SQLiteNIO/SQLiteCustomFunction.swift Outdated Show resolved Hide resolved
nil, nil, nil, nil, nil)

guard code == SQLITE_OK else {
// Assume a bug: there is no point throwing any error.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird comment above this code.

})

guard code == SQLITE_OK else {
// Assume a bug: there is no point throwing any error.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird comment above this bug.

assert(!aggregateContext.hasErrored) // assert SQLite behavior
do {
let arguments = (0..<Int(argc)).map { index in
SQLiteData(sqliteValue: argv.unsafelyUnwrapped[index]!)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no justification for unsafelyUnwrapped here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

aggregateContextBufferP.copyMemory(from: $0)
}
return aggregateContextU
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not wild about this function. I think there are a few places where it's being excessively clever.

First, I really don't love that it's a bit vague about what it's storing. In the initialization branch it stores the bytes of an OpaquePointer into the buffer, but in the other branch it just immediately type-puns to Unmanaged<AggregateContext>. That works, but it's not great: we should be consistent in the branches. I'd probably prefer we use OpaquePointer and do the full Unmanaged dance in both branches, rather than rely on Unmanaged being bitwise-takeable (though I think it probably is).

The thing I like much less is the detection of non-zero bytes in the buffer as a mechanism for working out if the memory is initialized. Fortunately for us, we don't need to do that. If we define the sqlite_aggregate_context as containing OpaquePointer? (or, alternatively, Unmanaged<AggregateContext>?, though I like that less) then we can use != nil to detect the uninitialised case. This is way safer and less prone to error, as well as being in line with how the API expects to be used in C code.

Finally, this function returns an Unmanaged<AggregateContext>, which sorta begs to be leaked. I'd rather that this function took a "consuming" parameter that decided whether it consumed the unbalanced retain or not.

Copy link

@groue groue Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at this review with the greatest interest 😋 This code was written 5+ years ago and never had the privilege of multiple pairs of eyes. If you don't mind, I'd be happy to learn from your experience here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what the reviewer is saying. I'm open to help for re-implementing this function 💜

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've taken a crack at it, hopefully Cory won't tell me I've made things even worse 😅

Sources/SQLiteNIO/SQLiteData.swift Show resolved Hide resolved
@danramteke
Copy link
Contributor Author

I have addressed most of the code comments from @gwynne & @Lukasa. Could you please allow the GitHub Action to run?

@gwynne gwynne requested a review from Lukasa December 6, 2022 00:18
@Austinpayne
Copy link
Contributor

@danramteke I'm confused, you appear to have reverted the changes of #35 in this PR. Was that intentional?

@gwynne
Copy link
Member

gwynne commented Feb 13, 2023

That's due to this branch not having been updated in a while. The conflicts need to be resolved; the seeming "revert" is just an artifact of that. @danramteke, can you get this up to date with the changes from main?

@gwynne
Copy link
Member

gwynne commented Feb 13, 2023

And also @Lukasa could you take a peek and let us know if your issues were resolved? 🙂

@danramteke
Copy link
Contributor Author

@Austinpayne yep, @gwynne is right, it's the result of merge conflicts

@danramteke
Copy link
Contributor Author

danramteke commented Feb 22, 2023

If anyone can help get this PR over the finish line, please lend a helping hand, as my bandwidth is limited at the moment.

Remaining tasks:

# Conflicts:
#	.github/workflows/test.yml
#	Package.swift
#	Sources/CSQLite/include/sqlite3.h
#	Sources/CSQLite/sqlite3.c
#	Sources/CSQLite/version.txt
#	Sources/SQLiteNIO/SQLiteConnection.swift
#	Sources/SQLiteNIO/SQLiteData.swift
#	Tests/SQLiteNIOTests/SQLiteNIOTests.swift
@gwynne
Copy link
Member

gwynne commented Feb 22, 2023

@danramteke @Lukasa Okay - I rebased onto main (as it is in your fork) to get rid of the mixed merge commits (merging from upstream main was getting messed up by the merge revert on the branch), fixed up the whitespace confusion, merged upstream main in so everything's completely up to date, and most importantly, I reimplemented the umanagedAggregateContext() method to - I hope! - address Cory's very valid concerns.

Cory, did I get it even remotely right? 🙏

@gwynne
Copy link
Member

gwynne commented Feb 22, 2023

P.S.: Yes, Cory, I know I didn't make the method optionally consuming like you wanted, but the behavior of SQLite with regards to aggregate contexts during xFinal is so weird I wasn't entirely good with the idea of tinkering more than necessary 😕

@gwynne gwynne requested review from gwynne and Lukasa and removed request for Lukasa February 22, 2023 19:58
@gwynne gwynne requested review from Lukasa and removed request for Lukasa February 22, 2023 19:59
@gwynne
Copy link
Member

gwynne commented Feb 22, 2023

Ugh, GitHub’s UI some days… 😬

@Lukasa
Copy link

Lukasa commented Feb 22, 2023

What happened to the indentation in SQLiteCustomFunction.swift? It's all over the place, the code is extraordinarily hard to read.

@gwynne
Copy link
Member

gwynne commented Feb 22, 2023

What happened to the indentation in SQLiteCustomFunction.swift? It's all over the place, the code is extraordinarily hard to read.

I thought I managed to fix that 😕 Heck with it, I’ll give it a swiftformat pass once I fix the 5.6 build

Sources/SQLiteNIO/SQLiteCustomFunction.swift Outdated Show resolved Hide resolved
Sources/SQLiteNIO/SQLiteData.swift Outdated Show resolved Hide resolved
@gwynne
Copy link
Member

gwynne commented Feb 24, 2023

@groue It seems as if I managed to stumble my way into what Cory was looking for in terms of improvements here - please feel free to use it in GRDB!

@groue
Copy link

groue commented Feb 24, 2023

@groue It seems as if I managed to stumble my way into what Cory was looking for in terms of improvements here - please feel free to use it in GRDB!

That's lovely 😊 I certainly follow this issue and your improvements. The SQLite api regarding custom functions and aggregates is awful (an exception because SQLite is usually a very nice C library). Thank you very much again.

@danramteke
Copy link
Contributor Author

Thank you @gwynne !!!

@gwynne gwynne removed the semver-minor Contains new APIs label Mar 13, 2023
@gwynne
Copy link
Member

gwynne commented Mar 13, 2023

Deliberately merging without a semver- label, will release manually.

@gwynne gwynne merged commit 596b680 into vapor:main Mar 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants