From 9196f5abae349a3a8fff514b159a3a6271ecd2e5 Mon Sep 17 00:00:00 2001 From: Marin Nozhchev Date: Mon, 13 Oct 2025 08:39:57 +0300 Subject: [PATCH 1/2] feat: basic Go SQL driver implementation --- driver.go | 39 +++++++++++++++++++++++++++++++++++++++ driver_integ_test.go | 23 +++++++++++++++++++++++ provider/airdb.go | 7 ++++--- provider/airsqltable.go | 11 ++++++----- register/register.go | 11 +++++++++++ 5 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 driver.go create mode 100644 driver_integ_test.go create mode 100644 register/register.go diff --git a/driver.go b/driver.go new file mode 100644 index 0000000..5c04887 --- /dev/null +++ b/driver.go @@ -0,0 +1,39 @@ +// Package airtablesql implements a Go SQL driver for Airtable +// Import package github.com/sclgo/airtable-sql/register to register +// a driver with default configuration automatically or use CreateDriver from this +// package and register it explicitly with sql.Register. +package airtablesql // import github.com/sclgo/airtable-sql + +import ( + "net/url" + + drivere "github.com/dolthub/go-mysql-server/driver" + "github.com/dolthub/go-mysql-server/sql" + "github.com/mehanizm/airtable" + "github.com/sclgo/airtable-sql/internal/errhelp" + "github.com/sclgo/airtable-sql/provider" + + "database/sql/driver" +) + +func CreateDriver() interface { + driver.Driver + driver.DriverContext +} { + return drivere.New(factory{}, nil) +} + +type factory struct{} + +// Resolve implements driver.Provider +func (factory) Resolve(dsn string, _ *drivere.Options) (string, sql.DatabaseProvider, error) { + dsnUri, err := url.Parse(dsn) + if err != nil { + return "", nil, errhelp.Errorf("could not parse DSN %s: %w", dsn, err) + } + key := dsnUri.Query().Get("key") + if key == "" { + return "", nil, errhelp.Errorf("could not find key in DSN %s", dsn) + } + return dsn, provider.New(airtable.NewClient(key)), nil +} diff --git a/driver_integ_test.go b/driver_integ_test.go new file mode 100644 index 0000000..dee34de --- /dev/null +++ b/driver_integ_test.go @@ -0,0 +1,23 @@ +package airtablesql_test + +import ( + "database/sql" + "os" + "testing" + + airtablesql "github.com/sclgo/airtable-sql" + "github.com/stretchr/testify/require" +) + +func TestCreateDriver(t *testing.T) { + t.Run("happy", func(t *testing.T) { + drv := airtablesql.CreateDriver() + cnct, err := drv.OpenConnector("airtable:?key=" + os.Getenv("AIRTABLE_API_KEY")) + require.NoError(t, err) + db := sql.OpenDB(cnct) + row := db.QueryRow("SELECT `Part Name` FROM `Order Assembly`.Parts") + var part string + require.NoError(t, row.Scan(&part)) + require.Equal(t, "Power Supply Unit", part) + }) +} diff --git a/provider/airdb.go b/provider/airdb.go index 7106989..8a823ec 100644 --- a/provider/airdb.go +++ b/provider/airdb.go @@ -9,8 +9,9 @@ import ( "github.com/mehanizm/airtable" ) -var _ sql.Database = (*AirDB)(nil) -var _ sql.Table = (*AirSqlTable)(nil) +var ( + _ sql.Database = (*AirDB)(nil) +) type AirDB struct { client *airtable.Client @@ -31,7 +32,7 @@ func (a *AirDB) GetTableInsensitive(ctx *sql.Context, tblName string) (sql.Table for _, tbl := range tbls.Tables { if strings.EqualFold(tblName, tbl.Name) { return AirSqlTable{ - tableClient: a.client.GetTable(a.dbId, tbl.Name), + tableClient: a.client.GetTable(a.dbId, tbl.Name), airSchema: tbl, schema: toSqlSchema(tbl), }, true, nil diff --git a/provider/airsqltable.go b/provider/airsqltable.go index 749191e..50a85d4 100644 --- a/provider/airsqltable.go +++ b/provider/airsqltable.go @@ -9,6 +9,12 @@ import ( "github.com/mehanizm/airtable" ) +var ( + _ sql.RowIter = (*basicRowIter)(nil) + _ sql.Disposable = (*basicRowIter)(nil) + _ sql.Table = (*AirSqlTable)(nil) +) + type AirSqlTable struct { tableClient *airtable.Table airSchema *airtable.TableSchema @@ -100,8 +106,3 @@ func (b *basicRowIter) Close(*sql.Context) error { b.recs = nil return nil } - -var ( - _ sql.RowIter = (*basicRowIter)(nil) - _ sql.Disposable = (*basicRowIter)(nil) -) diff --git a/register/register.go b/register/register.go new file mode 100644 index 0000000..4bab430 --- /dev/null +++ b/register/register.go @@ -0,0 +1,11 @@ +package register + +import ( + "database/sql" + + airtablesql "github.com/sclgo/airtable-sql" +) + +func init() { + sql.Register("airtable", airtablesql.CreateDriver()) +} From d94f11d8f0e9d71b43cc216214ec0d3eb8c81b2e Mon Sep 17 00:00:00 2001 From: Marin Nozhchev Date: Mon, 13 Oct 2025 21:47:39 +0300 Subject: [PATCH 2/2] Update driver_integ_test.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- driver_integ_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/driver_integ_test.go b/driver_integ_test.go index dee34de..aa0b0a9 100644 --- a/driver_integ_test.go +++ b/driver_integ_test.go @@ -11,6 +11,9 @@ import ( func TestCreateDriver(t *testing.T) { t.Run("happy", func(t *testing.T) { + if os.Getenv("AIRTABLE_API_KEY") == "" { + t.Skip("skipping integration test; AIRTABLE_API_KEY not set") + } drv := airtablesql.CreateDriver() cnct, err := drv.OpenConnector("airtable:?key=" + os.Getenv("AIRTABLE_API_KEY")) require.NoError(t, err)