facebookincubator / CG-SQL

CG/SQL is a compiler that converts a SQL Stored Procedure like language into C for SQLite. SQLite has no stored procedures of its own. CG/CQL can also generate other useful artifacts for testing and schema maintenance.
https://cgsql.dev/
MIT License
392 stars 34 forks source link

"would be nice" if cqlrt-cf was Swift-compatible #106

Closed jackpal closed 2 years ago

jackpal commented 2 years ago

I've written a Swift code generator, that uses the ordinary cqlrt due to issue #105.

But even if/when #105 is fixed, it's still difficult to use the cqlrt-cf with Swift due to the use of #defines. In order to use cg-sql from Swift I had to write C wrappers for these cqlrt macros:

const char * _Nonnull cql_string_ref_cstr(cql_string_ref _Nonnull str);
const void * _Nonnull swift_cql_get_blob_bytes(cql_blob_ref _Nonnull blob);
cql_uint32 swift_cql_get_blob_size(cql_blob_ref _Nonnull blob);
cql_hash_code swift_todo_tasks_row_hash(cql_result_set_ref _Nonnull result_set, cql_int32 row);
cql_bool swift_todo_tasks_row_equal(cql_result_set_ref _Nonnull rs1, cql_int32 row1, cql_result_set_ref _Nonnull rs2, cql_int32 row2);
extern cql_int32 swift_todo_tasks_data_types_count;
extern cql_int64 swift_CRC_todo_tasks;
cql_hash_code swift_get_b_row_hash(cql_result_set_ref _Nonnull result_set, cql_int32 row);
cql_bool swift_get_b_row_equal(cql_result_set_ref _Nonnull rs1, cql_int32 row1, cql_result_set_ref _Nonnull rs2, cql_int32 row2);
extern cql_int32 swift_get_b_data_types_count;
extern cql_int64 swift_CRC_get_b;

Basically, all the #defines in cqlrt.h are invisible to Swift, these are just the ones that need to be worked around for basic operations. Presumably as I flesh out the Swift runtime, all the cqlrt defines will need to be wrapped in functions or variables.

I assume the macros are used instead of functions to eke out a slight performance win. But the result is that Swift clients have to write (and call) these wrapper functions.

I wonder if you would consider making the sample runtime avoid using #defines, or alternately provide standard functions/variables for all the defines, so that the sample runtime could be used as-is from Swift...

ricomariani commented 2 years ago

If you have an exhaustive list of which macros need to be converted that'll be pretty easy.

What of this is in the generated objc code vs. what's in cqlrt_rt.c

We might need a different --rt type for swift compatible output.

For instance todo_tasks_row_equal is generated.

ricomariani commented 2 years ago
#define todo_tasks_row_hash(result_set, row) cql_result_set_get_meta((cql_result_set_ref)(result_set))->rowHash((cql_result_set_ref)(result_set), row)
#define todo_tasks_row_equal(rs1, row1, rs2, row2) \
cql_result_set_get_meta((cql_result_set_ref)(rs1))->rowsEqual( \
  (cql_result_set_ref)(rs1), \
  row1, \
  (cql_result_set_ref)(rs2), \
  row2)

This would require a codegen change to be swift compatible. We could generate inline functions along with the objc stuff. The swift probably shouldn't be calling the C directly.

There's some similar stuff in cqlrt_cf.h that can be changed pretty easily.

jackpal commented 2 years ago

Here are the macros that I hard-code manual expansions into Swift code in the code generator:

// Manually expanded to cast and call cql_retain
cql_string_retain
cql_bob_retain
cql_object_retain
cql_result_set_retain

// Manually expanded to cast and call cql_release
cql_string_release
cql_bob_release
cql_object_release
cql_result_set_release

// Ignored because implementation does nothing
cql_free_cstr

And here are functions that are generated:

#pragma once
#include "cqlrt.h"

/* Access primitive types. */
const char * _Nonnull cql_string_ref_cstr(cql_string_ref _Nonnull str);
const void * _Nonnull swift_cql_get_blob_bytes(cql_blob_ref _Nonnull blob);
cql_uint32 swift_cql_get_blob_size(cql_blob_ref _Nonnull blob);

/* Generated for a each result set  */
cql_hash_code swift_RESULT_SET_row_hash(cql_result_set_ref _Nonnull result_set, cql_int32 row);
cql_bool swift_RESULT_SET_row_equal(cql_result_set_ref _Nonnull rs1, cql_int32 row1, cql_result_set_ref _Nonnull rs2, cql_int32 row2);
extern cql_int32 swift_RESULT_SET_data_types_count; /* Could be inferred from json schema, I think, */
extern cql_int64 swift_CRC_RESULT_SET;
jackpal commented 2 years ago

I should mention that I have only done code gen for insert,update,select,delete. I haven't done code gen for the "object" type, yet.

Also, some defines like "data_types_count" are not needed because that information is also available in the JSON schema.

ricomariani commented 2 years ago

cql_free_cstr

should be doing something, maybe I forgot to wire it, but there's a dealloc macro there.


From: Jack Palevich @.> Sent: Sunday, January 2, 2022 8:58 AM To: facebookincubator/CG-SQL @.> Cc: Rico Mariani @.>; Comment @.> Subject: Re: [facebookincubator/CG-SQL] "would be nice" if cqlrt-cf was Swift-compatible (Issue #106)

I should mention that I have only done code gen for insert,update,select,delete. I haven't done code gen for the "object" type, yet.

Also, some defines like "CRC" are not needed because that information is also available in the JSON schema.

— Reply to this email directly, view it on GitHubhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Ffacebookincubator%2FCG-SQL%2Fissues%2F106%23issuecomment-1003745176&data=04%7C01%7C%7C9d0ee1b7511144ce06d508d9ce11201f%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767395208940184%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=JZKFAI%2FHQwHB0Ko2C9FYeVdud3dCMlEDoJFLOmK0woM%3D&reserved=0, or unsubscribehttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FAHC3V77IY4YGDH73EUX4K5DUUB737ANCNFSM5LCJA5RQ&data=04%7C01%7C%7C9d0ee1b7511144ce06d508d9ce11201f%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767395208940184%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=XvH60i3hxQtkBNarydfHh20%2F1QUCvCCuIsbUttC1TUk%3D&reserved=0. Triage notifications on the go with GitHub Mobile for iOShttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fapps.apple.com%2Fapp%2Fapple-store%2Fid1477376905%3Fct%3Dnotification-email%26mt%3D8%26pt%3D524675&data=04%7C01%7C%7C9d0ee1b7511144ce06d508d9ce11201f%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767395208940184%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=7nCfL6kzv17QYyCMEVI0g1xSJixH1yB3Bq4XDvrbTqw%3D&reserved=0 or Androidhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.github.android%26referrer%3Dutm_campaign%253Dnotification-email%2526utm_medium%253Demail%2526utm_source%253Dgithub&data=04%7C01%7C%7C9d0ee1b7511144ce06d508d9ce11201f%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767395208940184%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=%2B5TZchcU3cihynvWWE3IWoyTtgSyOXPcbU5NDdoOjzM%3D&reserved=0. You are receiving this because you commented.Message ID: @.***>

jackpal commented 2 years ago

(I'm referring to the c runtime, not the cf runtime... In the C runtime it's:

#define cql_free_cstr(cstr, str) 0
ricomariani commented 2 years ago

Yeah it turns out sometimes it's useful to have the CRC available for macros and such. The profiling macros especially find the CRC useful.

define cql_alloc_cstr(cstr, str) CF_STRING_CREATE_C_STRING(cstr, str)

define cql_free_cstr(cstr, str) CF_STRING_FREE_C_STRING(cstr, str)

I expected you would be using the above.

Are you able to get the regular CQLRT to work with swift, that seems like it would end badly, because for instance, strings do not interoperate.

Perhaps I don't fully understand what you're doing 😄


From: Jack Palevich @.> Sent: Sunday, January 2, 2022 9:05 AM To: facebookincubator/CG-SQL @.> Cc: Rico Mariani @.>; Comment @.> Subject: Re: [facebookincubator/CG-SQL] "would be nice" if cqlrt-cf was Swift-compatible (Issue #106)

(I'm referring to the c runtime, not the cf runtime... In the C runtime it's:)

define cql_free_cstr(cstr, str) 0

— Reply to this email directly, view it on GitHubhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Ffacebookincubator%2FCG-SQL%2Fissues%2F106%23issuecomment-1003746128&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=AvcxWfaqhVtPHv4cU766hYw4DFsD2sOQFpmuJZOZsXA%3D&reserved=0, or unsubscribehttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FAHC3V7ZHWOYJKR4KH67URWLUUCAWPANCNFSM5LCJA5RQ&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=MJW6BKIK1TA4GucHYUweLEBQhcdmUNT5O1ejuZBe5NM%3D&reserved=0. Triage notifications on the go with GitHub Mobile for iOShttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fapps.apple.com%2Fapp%2Fapple-store%2Fid1477376905%3Fct%3Dnotification-email%26mt%3D8%26pt%3D524675&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=DlUt4ifQJog%2F682LsZUqK%2F9QdaLtCZDpGvbv66gbPAA%3D&reserved=0 or Androidhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.github.android%26referrer%3Dutm_campaign%253Dnotification-email%2526utm_medium%253Demail%2526utm_source%253Dgithub&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=phR6QkXjwepLYLR6MzbH%2BRHoxxc8%2FQKUy6vVOwq9wHU%3D&reserved=0. You are receiving this because you commented.Message ID: @.***>

ricomariani commented 2 years ago

Oh maybe you're making you're own wrapper from the JSON?


From: Rico Mariani @.> Sent: Sunday, January 2, 2022 9:32 AM To: facebookincubator/CG-SQL @.>; facebookincubator/CG-SQL @.> Cc: Comment @.> Subject: Re: [facebookincubator/CG-SQL] "would be nice" if cqlrt-cf was Swift-compatible (Issue #106)

Yeah it turns out sometimes it's useful to have the CRC available for macros and such. The profiling macros especially find the CRC useful.

define cql_alloc_cstr(cstr, str) CF_STRING_CREATE_C_STRING(cstr, str)

define cql_free_cstr(cstr, str) CF_STRING_FREE_C_STRING(cstr, str)

I expected you would be using the above.

Are you able to get the regular CQLRT to work with swift, that seems like it would end badly, because for instance, strings do not interoperate.

Perhaps I don't fully understand what you're doing 😄


From: Jack Palevich @.> Sent: Sunday, January 2, 2022 9:05 AM To: facebookincubator/CG-SQL @.> Cc: Rico Mariani @.>; Comment @.> Subject: Re: [facebookincubator/CG-SQL] "would be nice" if cqlrt-cf was Swift-compatible (Issue #106)

(I'm referring to the c runtime, not the cf runtime... In the C runtime it's:)

define cql_free_cstr(cstr, str) 0

— Reply to this email directly, view it on GitHubhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Ffacebookincubator%2FCG-SQL%2Fissues%2F106%23issuecomment-1003746128&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=AvcxWfaqhVtPHv4cU766hYw4DFsD2sOQFpmuJZOZsXA%3D&reserved=0, or unsubscribehttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FAHC3V7ZHWOYJKR4KH67URWLUUCAWPANCNFSM5LCJA5RQ&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=MJW6BKIK1TA4GucHYUweLEBQhcdmUNT5O1ejuZBe5NM%3D&reserved=0. Triage notifications on the go with GitHub Mobile for iOShttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fapps.apple.com%2Fapp%2Fapple-store%2Fid1477376905%3Fct%3Dnotification-email%26mt%3D8%26pt%3D524675&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=DlUt4ifQJog%2F682LsZUqK%2F9QdaLtCZDpGvbv66gbPAA%3D&reserved=0 or Androidhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.github.android%26referrer%3Dutm_campaign%253Dnotification-email%2526utm_medium%253Demail%2526utm_source%253Dgithub&data=04%7C01%7C%7C8b45ae1418704f7b110208d9ce121cfc%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637767399451597450%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=phR6QkXjwepLYLR6MzbH%2BRHoxxc8%2FQKUy6vVOwq9wHU%3D&reserved=0. You are receiving this because you commented.Message ID: @.***>

jackpal commented 2 years ago

Strings interop, it's just a lot of code. But yes, I am generating it from the JSON using a 600 line python script.

See "todoAdd" below:

import Foundation

import libTodo

public typealias DB = OpaquePointer

public enum Error : Swift.Error {
    case result(code: Int32)
}

fileprivate func check(_ code: Int32) throws {
    if code != SQLITE_OK {
        throw Error.result(code:code)
    }
}

public func todoCreateTables(db: DB) throws {
    try check(todo_create_tables(db))
}

public func todoAdd(db: DB, description: String, done: Bool) throws {
    let _1_description = description as NSString
      let _2_description = UnsafeMutablePointer<CChar>(mutating: _1_description.utf8String)!
      let _3_description = cql_string_ref_new(_2_description)
      defer { cql_release(UnsafeMutablePointer(OpaquePointer(_3_description)))}
    try check(todo_add(db, _3_description, cql_bool(done ? 1 : 0)))
}

public func todoSetDone(db: DB, rowID: Int32, done: Bool) throws {
    try check(todo_setDone(db, rowID, cql_bool(done ? 1 : 0)))
}

public func todoDelete(db: DB, rowID: Int32) throws {
    try check(todo_delete(db, rowID))
}

public class AllTasks : RandomAccessCollection {
    public typealias Index = Int32
    public typealias Indices = CountableRange<Int32>

    public struct Element : Hashable {
        let resultSet: AllTasks
        public let row: Int32
        public var rowID: Int64 {
            let v = all_tasks_get_rowid(resultSet.result_set_ref, row)
            return v
        }
        public var description: String {
            let v = all_tasks_get_description(resultSet.result_set_ref, row)
            return String(cString:cql_string_ref_cstr(v))
        }
        public var done: Bool {
            let v = all_tasks_get_done(resultSet.result_set_ref, row)
            return v != 0
        }

        // Hashable
        public static func == (lhs: Element, rhs: Element) -> Bool {
            return swift_all_tasks_row_equal(UnsafeMutablePointer(lhs.resultSet.result_set_ref), lhs.row,
            UnsafeMutablePointer(rhs.resultSet.result_set_ref), rhs.row) != 0
        }

        public func hash(into hasher: inout Hasher) {
            hasher.combine(swift_all_tasks_row_hash(UnsafeMutablePointer(resultSet.result_set_ref), row))
        }

    }

    // RandomAccessCollection
    public subscript(index: Int32) -> Element {
        get { Element(resultSet:self, row:index) }
    }

    public var startIndex : Index { 0 }
    public var endIndex : Index {
        all_tasks_result_count(result_set_ref)
    }

    public static var storedProcedureName : String {
        String(cString:cql_string_ref_cstr(all_tasks_stored_procedure_name))
    }

    public static var dataTypesCount: Int32 {
        swift_all_tasks_data_types_count
    }

    public static var CRC : Int64 {
        swift_CRC_all_tasks
    }

    public static let usesTables : Set = ["tasks"]

    private var result_set_ref: all_tasks_result_set_ref!

    public init(db: DB) throws {
        try check(all_tasks_fetch_results(db, &result_set_ref))
        cql_retain(UnsafeMutablePointer(result_set_ref))

    }

    deinit {
        cql_release(UnsafeMutablePointer(result_set_ref))
    }
}
jackpal commented 2 years ago

(I certainly would prefer to use the cf runtime, once it works with ARC.)

jackpal commented 2 years ago

This comment isn't really related to this bug, but as long as I have shown my current Swift code generator output, I wanted to mention that I am still considering how best to generate code for result sets. There should probably be a Swift protocol that all result sets implement. That in turn means there should be a separate Swift Package Manager package to hold the "CG-SQL Swift Runtime", that defines that protocol. I guess that's also a convenient place to park the SQLiteError type. And even the cqlrt code that's common to all compilation units.

There are a large number of Swift SQLite runtimes like GRDB and SQLite. They have some level of interoperability due to the stringly-typed nature of sqlite and they all use OpaquePointer for the db connection.

I think that means that a swift package created by cql could be used "as is" with various runtimes.

ricomariani commented 2 years ago

I think it would be a lot simpler to use the _cf version with swift really. It wouldn't be so hard to make the helper functions be all "not macros". I can do that after my vacation if you like. Or you could shoot me some pseudo diffs if you want to take a cut at it. I don't think anyone has tried CQL/Swift interop yet so you're breaking ground. I guess the fact that the simple strings are just thinly wrapped C strings makes some things easier/clearer. But CF string are toll-free and this is a lot of copying.

ricomariani commented 2 years ago

Oh a design point of the basic model was that we didn't want a lot of classes because you can easily end up with zillions of these result sets and adding a zillion more classes to your package costs a lot of space. Hence the econo-choices. There's no reason that you couldn't do something more OOPish. But if you plan to have a lot of stored procedures you might want to consider the economy of classes. Messenger has thousands of stored procs... Just the selectors for all that would add a ton of space.

ricomariani commented 2 years ago

The beauty of wrapping the core/cheap stuff with JSON driven stuff is you really can have it "your way" for just a few hundred lines of code which is not bad at all really :D

ricomariani commented 2 years ago

You could try using the fix I have for 105 and see if that comes out better.

jackpal commented 2 years ago

Yes indeed, I am going to try switching to using the cf runtime today. Between your fixes and Xcode's debugging tools I'm sure it will work pretty well.

Hope you have a great vacation!

I agree that classes / ORM are an "attractive nuisance" for sql. You start using them and pretty soon you need a degree in philosophy to design your class hierarchy.

Did you ever look at https://github.com/wildthink/freds_bank ? That guy loves playing around with different ways of using SQL with iOS, and when he heard about "Project Lightspeed", but before you released cg-sql, he was inspired to write a toy framework that worked the way he thought "Project Lightspeed" might work. His code uses no generated code or per-procedure classes. Everything's done in SQL (making liberal use of JSON1 extensions.), and on the UX side he uses SQL Views for his view models, and his ViewControllers are reusable, and configured in terms of Interface builder strings with the format "table/column/filter". I find that an interesting design point!

I'm also looking at iOS 15's DataFrame (https://developer.apple.com/documentation/tabulardata), which can be used as a no-code-gen result set....

jackpal commented 2 years ago

I wonder if the lowest-overhead way to to Swift interop is to add NS_SWIFT_NAME annotations to the obj-c generated code to turn the snake-case identifiers into Swift-code-style camel-case identifiers. I'll experiment with that.

For SwiftUI purposes the core reason for providing some sort of class for a result set is to interop with the List and ForEach views:

import Todo

struct TaskList : View {
  @Query private var tasks = myQueryProc()
  var body: some View {
    Text("Tasks: \(tasks.count)")
    List(tasks) {row in
      Text(row.description)
    }
  }
}

I'm trying to figure out how to make that all work. The GRDBQuery project does it in a generic way, it might be possible to interoperate with that.

jackpal commented 2 years ago

I converted my Swift code generator to use cqlrt-cf (the ARC-safe patched version.).

Overall things are looking pretty good. I've included generated output for this sql file:

create proc todo_create_tables()
begin

  -- All non-null cgsql column types
  create table a (
    t text not null,
    b bool not null,
    i integer not null,
    l long not null,
    r real not null,
    bl blob not null
  );

  -- All nullable cgsql column types
  create table b(
    t text,
    b bool,
    i integer,
    l long,
    r real,
    bl blob
  );

end;

create proc a_add(t text not null, b bool not null, i integer not null, l long not null, r real not null, bl blob not null)
begin
  insert into a values(t, b, i, l, r, bl);
end;

create proc b_add(t text, b bool, i integer, l long, r real, bl blob)
begin
  insert into b values(t, b, i, l, r, bl);
end;

create proc all_a()
begin
  select rowid, t, b, i, l, r, bl from a order by rowid;
end;

create proc all_b()
begin
  select rowid, t, b, i, l, r, bl from b order by rowid;
end;

create proc a_set_b(rowid_ integer not null, b_ bool not null)
begin
  update a set b = b_ where rowid == rowid_;
end;

create proc a_delete(rowid_ integer not null)
begin
  delete from a where rowid == rowid_;
end;

The generated swift helper file is almost entirely gone. The remaining info is:

#pragma once

#import "testgen_objc.h"

@interface CGS_all_a
@end

static inline cql_int32 get_all_a_data_types_count() { return all_a_data_types_count; }
static inline cql_int64 get_CRC_all_a() { return CRC_all_a; };

@interface CGS_all_b
@end

static inline cql_int32 get_all_b_data_types_count() { return all_b_data_types_count; }
static inline cql_int64 get_CRC_all_b() { return CRC_all_b; };

And the generated Swift code looks like this:

import Foundation

import libTestGen

public enum SQLError : Swift.Error {
    case result(code: Int32)
}

fileprivate func check(_ code: Int32) throws {
    if code != SQLITE_OK {
        throw SQLError.result(code:code)
    }
}

public func todoCreateTables(db: OpaquePointer) throws {
    try check(todo_create_tables(db))
}

public func aAdd(db: OpaquePointer, t: String, b: Bool, i: Int32, l: Int64, r: Double, bl: Data) throws {
    try check(a_add(db, t as NSString, b, i, l, r, bl as NSData))
}

public func bAdd(db: OpaquePointer, t: String?, b: Bool?, i: Int32?, l: Int64?, r: Double?, bl: Data?) throws {
    let _1_b = cql_nullable_bool(is_null:DarwinBoolean(b == nil), value:cql_bool((b ?? false)))
    let _1_i = cql_nullable_int32(is_null:DarwinBoolean(i == nil), value:cql_int32((i ?? 0)))
    let _1_l = cql_nullable_int64(is_null:DarwinBoolean(l == nil), value:cql_int64((l ?? 0)))
    let _1_r = cql_nullable_double(is_null:DarwinBoolean(r == nil), value:cql_double((r ?? 0.0)))
    try check(b_add(db, t as NSString?, _1_b, _1_i, _1_l, _1_r, bl as NSData?))
}

public func aSetB(db: OpaquePointer, rowID: Int32, b: Bool) throws {
    try check(a_set_b(db, rowID, b))
}

public func aDelete(db: OpaquePointer, rowID: Int32) throws {
    try check(a_delete(db, rowID))
}

public class AllA : RandomAccessCollection {
    public typealias Index = Int32
    public typealias Indices = CountableRange<Int32>

    public struct Element : Hashable {
        let resultSet: AllA
        public let row: Int32
        public var rowID: Int64 {
            CGS_all_a_get_rowid(resultSet.result_set, row)
        }
        public var t: String {
            CGS_all_a_get_t(resultSet.result_set, row)
        }
        public var b: Bool {
            CGS_all_a_get_b(resultSet.result_set, row)
        }
        public var i: Int32 {
            CGS_all_a_get_i(resultSet.result_set, row)
        }
        public var l: Int64 {
            CGS_all_a_get_l(resultSet.result_set, row)
        }
        public var r: Double {
            CGS_all_a_get_r(resultSet.result_set, row)
        }
        public var bl: Data {
            CGS_all_a_get_bl(resultSet.result_set, row)
        }

        // Hashable
        public static func == (lhs: Element, rhs: Element) -> Bool {
            CGS_all_a_row_equal(lhs.resultSet.result_set, lhs.row,
                rhs.resultSet.result_set, rhs.row)
        }

        public func hash(into hasher: inout Hasher) {
            hasher.combine(CGS_all_a_row_hash(resultSet.result_set, row))
        }

    }

    // RandomAccessCollection
    public subscript(index: Int32) -> Element {
        get { Element(resultSet:self, row:index) }
    }

    public var startIndex : Index { 0 }
    public var endIndex : Index {
        CGS_all_a_result_count(result_set)
    }

    public static let storedProcedureName =
        all_a_stored_procedure_name.takeUnretainedValue() as String

    public static let dataTypesCount =
        get_all_a_data_types_count()

    public static let CRC = get_CRC_all_a()

    public static let usesTables : Set = ["a"]

    private var result_set_ref: Unmanaged<all_a_result_set_ref>?
    private var result_set: CGS_all_a!

    public init(db: OpaquePointer) throws {
        try check(all_a_fetch_results(db, &result_set_ref))
        result_set = CGS_all_a_from_all_a(result_set_ref!.takeUnretainedValue())
    }

    deinit {
        cql_release(result_set_ref!.takeUnretainedValue())
    }
}

public class AllB : RandomAccessCollection {
    public typealias Index = Int32
    public typealias Indices = CountableRange<Int32>

    public struct Element : Hashable {
        let resultSet: AllB
        public let row: Int32
        public var rowID: Int64 {
            CGS_all_b_get_rowid(resultSet.result_set, row)
        }
        public var t: String? {
            CGS_all_b_get_t(resultSet.result_set, row) as String?
        }
        public var b: Bool? {
            CGS_all_b_get_b(resultSet.result_set, row)?.boolValue
        }
        public var i: Int32? {
            CGS_all_b_get_i(resultSet.result_set, row)?.int32Value
        }
        public var l: Int64? {
            CGS_all_b_get_l(resultSet.result_set, row)?.int64Value
        }
        public var r: Double? {
            CGS_all_b_get_r(resultSet.result_set, row)?.doubleValue
        }
        public var bl: Data? {
            CGS_all_b_get_bl(resultSet.result_set, row) as Data?
        }

        // Hashable
        public static func == (lhs: Element, rhs: Element) -> Bool {
            CGS_all_b_row_equal(lhs.resultSet.result_set, lhs.row,
                rhs.resultSet.result_set, rhs.row)
        }

        public func hash(into hasher: inout Hasher) {
            hasher.combine(CGS_all_b_row_hash(resultSet.result_set, row))
        }

    }

    // RandomAccessCollection
    public subscript(index: Int32) -> Element {
        get { Element(resultSet:self, row:index) }
    }

    public var startIndex : Index { 0 }
    public var endIndex : Index {
        CGS_all_b_result_count(result_set)
    }

    public static let storedProcedureName =
        all_b_stored_procedure_name.takeUnretainedValue() as String

    public static let dataTypesCount =
        get_all_b_data_types_count()

    public static let CRC = get_CRC_all_b()

    public static let usesTables : Set = ["b"]

    private var result_set_ref: Unmanaged<all_b_result_set_ref>?
    private var result_set: CGS_all_b!

    public init(db: OpaquePointer) throws {
        try check(all_b_fetch_results(db, &result_set_ref))
        result_set = CGS_all_b_from_all_b(result_set_ref!.takeUnretainedValue())
    }

    deinit {
        cql_release(result_set_ref!.takeUnretainedValue())
    }
}
jackpal commented 2 years ago

In the generated swift-helper.h file, you'll see that I had to create @interfaces for the result set classes. Otherwise Swift didn't think the classes existed. (@class declarations are not enough.)

I think the only ugly code is related to creating and releasing result sets:

    private var result_set_ref: Unmanaged<all_a_result_set_ref>?
    private var result_set: CGS_all_a!

    public init(db: DB) throws {
        try check(all_a_fetch_results(db, &result_set_ref))
        result_set = CGS_all_a_from_all_a(result_set_ref!.takeUnretainedValue())
    }

I guess it would be nice if the CGS_RESULT_SET class held onto the RESULT_SET_ref cgl struct, and acted like a normal retained ARC Objective-C class.

jackpal commented 2 years ago

It seems inefficient for Swift to use the NSNumber APIs to get the nullable numeric types from the result set. I think I'll switch back to the C runtime for those.

ricomariani commented 2 years ago

You can of course mix and match, there's no law that says you have to use all the objc wrappers. Personally, NSNumber usually drives me crazy :D But I'm an old fellow :D

We have a conversion from the ref to the NS* (i.e. this sort of business)

        try check(all_a_fetch_results(db, &result_set_ref))
        result_set = CGS_all_a_from_all_a(result_set_ref!.takeUnretainedValue())

because we don't want to generate yet another mirrored fetch_results method that's just going to forward all the args. Though it would be possible to do so. "CGS_all_a_from_all_a" should be giving you a +1. You could do what I do in demo and hold the ARC version and immediately release the non-arc version. I think that's what the above amounts to. Maybe the new demo main will clarify this a bit. I should land soonish, it's just Sunday and there's nobody around to review it :D

jackpal commented 2 years ago

FWIW I think I have my code generator in pretty good shape for a first release.

I added support for exposing the underlying sqlite3 prepared statement version of the CG/SQL fetch procs. I did this for three reasons:

  1. I wanted to feed the prepared statement into a third party SQL library.
  2. It makes it possible to "stream" the data, for when we're operating on arbitrarily large number of rows, and we don't care about locking the db for long periods of time.
  3. It makes it easier to write generic code that handles arbitrary result sets, without having to use templates.

Anyway, now that things are looking in good shape, I'm working through my company's open source permission process, when that's done I plan publish the code generator as a Github project. I'll let you know when I do that, in case you'd like to take a look.

Thanks for all your help!

ricomariani commented 2 years ago

Sounds fabulous. I'm back at work next week but hopefully the cqlrt_cf fixes will land today

Sent via the Samsung Galaxy S8+, an AT&T 5G Evolution capable smartphone Get Outlook for Androidhttps://aka.ms/AAb9ysg


From: Jack Palevich @.> Sent: Tuesday, January 4, 2022 12:38:17 AM To: facebookincubator/CG-SQL @.> Cc: Rico Mariani @.>; Comment @.> Subject: Re: [facebookincubator/CG-SQL] "would be nice" if cqlrt-cf was Swift-compatible (Issue #106)

FWIW I think I have my code generator in pretty good shape for a first release.

I added support for exposing the underlying sqlite3 prepared statement version of the CG/SQL fetch procs. I did this for three reasons:

  1. I wanted to feed the prepared statement into a third party SQL library.
  2. It makes it possible to "stream" the data, for when we're operating on arbitrarily large number of rows, and we don't care about locking the db for long periods of time.
  3. It makes it easier to write generic code that handles arbitrary result sets, without having to use templates.

Anyway, now that things are looking in good shape, I'm working through my company's open source permission process, when that's done I plan publish the code generator as a Github project. I'll let you know when I do that, in case you'd like to take a look.

Thanks for all your help!

— Reply to this email directly, view it on GitHubhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Ffacebookincubator%2FCG-SQL%2Fissues%2F106%23issuecomment-1004617236&data=04%7C01%7C%7Cde2e4a1015174fe43e3b08d9cf5d8ebe%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637768823000458028%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=Nv6zu%2B%2BOLYfomRfh5laK2Z7ICQ1r%2BDmfmFdjXaQIuXY%3D&reserved=0, or unsubscribehttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FAHC3V743CIRI3BKVDGCED2LUUKWXTANCNFSM5LCJA5RQ&data=04%7C01%7C%7Cde2e4a1015174fe43e3b08d9cf5d8ebe%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637768823000458028%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=JU1e6kuasIAZHxD%2FGRxOHIHBzKONvuASs0hqi%2BAyQBY%3D&reserved=0. Triage notifications on the go with GitHub Mobile for iOShttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fapps.apple.com%2Fapp%2Fapple-store%2Fid1477376905%3Fct%3Dnotification-email%26mt%3D8%26pt%3D524675&data=04%7C01%7C%7Cde2e4a1015174fe43e3b08d9cf5d8ebe%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637768823000458028%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=XsJz%2FlpxS1OhJbFpYgD%2FQMrZWtGbJQrJcxwgPqJW2xE%3D&reserved=0 or Androidhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.github.android%26referrer%3Dutm_campaign%253Dnotification-email%2526utm_medium%253Demail%2526utm_source%253Dgithub&data=04%7C01%7C%7Cde2e4a1015174fe43e3b08d9cf5d8ebe%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637768823000458028%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=fyUidxCtEFBONGsz%2F1iRYBpqc297GF3WX1y5ylplJJE%3D&reserved=0. You are receiving this because you commented.Message ID: @.***>

ricomariani commented 2 years ago

https://github.com/facebookincubator/CG-SQL/pull/108 should help with the cqlrt_cf.h side of things.

ricomariani commented 2 years ago

OK this is now pretty long and has wandered pretty far. I'm going to start a new issue with just what's left to do and close this one. Stand by.

Basically the remaining stuff is this kind of business.

#define todo_tasks_row_hash(result_set, row) cql_result_set_get_meta((cql_result_set_ref)(result_set))->rowHash((cql_result_set_ref)(result_set), row)
#define todo_tasks_row_equal(rs1, row1, rs2, row2) \
cql_result_set_get_meta((cql_result_set_ref)(rs1))->rowsEqual( \
  (cql_result_set_ref)(rs1), \
  row1, \
  (cql_result_set_ref)(rs2), \
  row2)

We need a way to access that stuff.

ricomariani commented 2 years ago

Well actually, what I was going to propose was that we generate a CGS wrapper for these so that you don't have to call the C version from swift at all.

But we already do that:

static inline NSUInteger CGS_todo_tasks_row_hash(CGS_todo_tasks *resultSet, int32_t row)
{
  return todo_tasks_row_hash(todo_tasks_from_CGS_todo_tasks(resultSet), row);
}

static inline BOOL CGS_todo_tasks_row_equal(CGS_todo_tasks *resultSet1, int32_t row1, CGS_todo_tasks *resultSet2, int32_t row2)
{
  return todo_tasks_row_equal(todo_tasks_from_CGS_todo_tasks(resultSet1), row1, todo_tasks_from_CGS_todo_tasks(resultSet2), row2);
}

So I think there's nothing left to do?

ricomariani commented 2 years ago

So given that I think there's nothing left, if you disagree can you open a new issue with just what is left? :D

I'll give you some time to object and then close this. I do expect you'll have to make some changes because you probably had workarounds for the lack of PR #108

jackpal commented 2 years ago

Sorry for not being clear. Here's what currently remains in my swifthelpers.h file:

pragma once

#import "todo_objc.h"

extern CQL_WARN_UNUSED cql_code all_tasks(sqlite3 *_Nonnull _db_, sqlite3_stmt *_Nullable *_Nonnull _result_stmt);

@interface CGS_all_tasks
@end

static inline cql_int32 get_all_tasks_data_types_count() { return all_tasks_data_types_count; }
static inline cql_int64 get_CRC_all_tasks() { return CRC_all_tasks; };

There are 4 items here.

Category 1: critical stuff that is required for Swift Interop:

@interface CGS_all_tasks
@end

This is required, otherwise Swift doesn't import the CGS_all_tasks class.

Category 2: Stuff that's required for less important features

static inline cql_int64 get_CRC_all_tasks() { return CRC_all_tasks; };

The CRC is not that important for toy apps, but this is the only way to make it available to Swift.

Category 3: Stuff that can be synthesized by the Swift generator from the json file

static inline cql_int32 get_all_tasks_data_types_count() { return all_tasks_data_types_count; }

As far as I can tell, this is just "number of columns in the result set", but maybe I am wrong.

Category 4: Experimental stuff.

extern CQL_WARN_UNUSED cql_code all_tasks(sqlite3 *_Nonnull _db_, sqlite3_stmt *_Nullable *_Nonnull _result_stmt);

This is something I think you ought to expose in the C and Objective-C APIs, but you don't currently, so there's no reason to expect you to add it just for Swift.

ricomariani commented 2 years ago

ok that's pretty clear.

Opening new issue for discussion of what's left.

ricomariani commented 2 years ago

https://github.com/facebookincubator/CG-SQL/issues/109

Has what's left in a nice clear form.