diff --git a/.gitignore b/.gitignore index 05ca3b9..23186d3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ go.work.sum # env file .env -/coverage \ No newline at end of file +/coverage + +/.idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/go.go b/go.go deleted file mode 100644 index 643f397..0000000 --- a/go.go +++ /dev/null @@ -1,5 +0,0 @@ -package template - -func hello() string { - return "hello world" -} diff --git a/go.mod b/go.mod index 627d7a6..e859e82 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,43 @@ -module github.com/sclgo/template +module github.com/sclgo/airtable-sql go 1.24.1 -require github.com/stretchr/testify v1.10.0 +require ( + github.com/ansel1/merry v1.8.1 + github.com/ansel1/merry/v2 v2.2.3 + github.com/dolthub/go-mysql-server v0.20.0 + github.com/mehanizm/airtable v0.3.4 + github.com/murfffi/gorich v0.3.0 + github.com/samber/lo v1.52.0 + github.com/stretchr/testify v1.11.1 +) require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect + github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad // indirect + github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect + github.com/dolthub/vitess v0.0.0-20250512224608-8fb9c6ea092c // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/lestrrat-go/strftime v1.0.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/tetratelabs/wazero v1.8.2 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/grpc v1.65.0-dev // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 713a0b4..74a3456 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,91 @@ +github.com/ansel1/merry v1.8.1 h1:z2o6oeJiJ7WNuBp6XAW6BQScBl6vULWxGdw5A/BHJgQ= +github.com/ansel1/merry v1.8.1/go.mod h1:wJVu1mHEtEUWq5zTTX9RiWjcE+xL8y7BGYl2VTYdP7M= +github.com/ansel1/merry/v2 v2.2.3 h1:/gBjiifpoymj+iV/8QApOET6Q4++DZJp55VR6fcHkIQ= +github.com/ansel1/merry/v2 v2.2.3/go.mod h1:Rs65Tv8RrdygaFCkV2VqLBTFe6HYIHFzEZRzvuIP0PU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= +github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad h1:66ZPawHszNu37VPQckdhX1BPPVzREsGgNxQeefnlm3g= +github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= +github.com/dolthub/go-mysql-server v0.20.0 h1:oB1WXD5TwdjhdyJDbF6VgVxyEbCevDRok9yEXefpoyI= +github.com/dolthub/go-mysql-server v0.20.0/go.mod h1:5ZdrW0fHZbz+8CngT9gksqSX4H3y+7v1pns7tJCEpu0= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= +github.com/dolthub/vitess v0.0.0-20250512224608-8fb9c6ea092c h1:imdag6PPCHAO2rZNsFoQoR4I/vIVTmO/czoOl5rUnbk= +github.com/dolthub/vitess v0.0.0-20250512224608-8fb9c6ea092c/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= +github.com/mehanizm/airtable v0.3.4 h1:2ny8QN+O2YIs0rBXn61OAUlsBXaLDPsBhVILeWZBBNo= +github.com/mehanizm/airtable v0.3.4/go.mod h1:ucwKW2iPJoEK9dIL7ueCaDdjClpG6pplAOGabgJtoLg= +github.com/murfffi/gorich v0.3.0 h1:cRsCCTD0A2eyiAjeSMwyOkjALDsVzgJO0ghY+cXsDJY= +github.com/murfffi/gorich v0.3.0/go.mod h1:fozPmSzPmc1r0xnNtk3HE6xxxlzlBqo4rlChD/iJZcM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.65.0-dev h1:aHJBb+Hz0geqgMFGwU6fuHrgXrIq2/vO7kwpjfJMPzk= +google.golang.org/grpc v1.65.0-dev/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= +gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go_test.go b/go_test.go deleted file mode 100644 index e29f9c7..0000000 --- a/go_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package template - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestHello(t *testing.T) { - require.Equal(t, "hello world", hello()) -} diff --git a/internal/errhelp/merry.go b/internal/errhelp/merry.go new file mode 100644 index 0000000..4f62b30 --- /dev/null +++ b/internal/errhelp/merry.go @@ -0,0 +1,17 @@ +package errhelp + +import ( + "fmt" + + "github.com/ansel1/merry/v2" +) + +func Errorf(format string, a ...any) error { + return merry.Wrap(fmt.Errorf(format, a...)) +} + +type FuncCloser func() error + +func (f FuncCloser) Close() error { + return f() +} diff --git a/provider/airdb.go b/provider/airdb.go new file mode 100644 index 0000000..7106989 --- /dev/null +++ b/provider/airdb.go @@ -0,0 +1,78 @@ +package provider + +import ( + "strings" + + "github.com/ansel1/merry/v2" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + "github.com/mehanizm/airtable" +) + +var _ sql.Database = (*AirDB)(nil) +var _ sql.Table = (*AirSqlTable)(nil) + +type AirDB struct { + client *airtable.Client + dbId string + name string + schema *airtable.BaseConfig +} + +func (a *AirDB) Name() string { + return a.name +} + +func (a *AirDB) GetTableInsensitive(ctx *sql.Context, tblName string) (sql.Table, bool, error) { + tbls, err := a.schema.GetTablesContext(ctx) + if err != nil { + return nil, false, err + } + for _, tbl := range tbls.Tables { + if strings.EqualFold(tblName, tbl.Name) { + return AirSqlTable{ + tableClient: a.client.GetTable(a.dbId, tbl.Name), + airSchema: tbl, + schema: toSqlSchema(tbl), + }, true, nil + } + } + return nil, false, nil +} + +func toSqlSchema(tbl *airtable.TableSchema) sql.Schema { + schema := sql.Schema{} + for _, field := range tbl.Fields { + schema = append(schema, &sql.Column{ + Name: field.Name, + Type: toSqlType(field.Type), + Comment: field.Description, + }) + } + return schema +} + +func toSqlType(_ string) sql.Type { + return types.Text +} + +func (a *AirDB) GetTableNames(ctx *sql.Context) ([]string, error) { + tbls, err := a.schema.GetTablesContext(ctx) + if err != nil { + return nil, merry.Wrap(err) + } + names := make([]string, 0) + for _, tbl := range tbls.Tables { + names = append(names, tbl.Name) + } + return names, nil +} + +func NewAirDB(name string, dbId string, client *airtable.Client) *AirDB { + return &AirDB{ + client: client, + dbId: dbId, + name: name, + schema: client.GetBaseSchema(dbId), + } +} diff --git a/provider/airsqltable.go b/provider/airsqltable.go new file mode 100644 index 0000000..749191e --- /dev/null +++ b/provider/airsqltable.go @@ -0,0 +1,107 @@ +package provider + +import ( + "io" + + "github.com/ansel1/merry/v2" + "github.com/dolthub/go-mysql-server/memory" + "github.com/dolthub/go-mysql-server/sql" + "github.com/mehanizm/airtable" +) + +type AirSqlTable struct { + tableClient *airtable.Table + airSchema *airtable.TableSchema + schema sql.Schema +} + +func (a AirSqlTable) Name() string { + return a.airSchema.Name +} + +func (a AirSqlTable) String() string { + return a.Name() +} + +func (a AirSqlTable) Schema() sql.Schema { + return a.schema +} + +func (a AirSqlTable) Collation() sql.CollationID { + return sql.Collation_binary +} + +func (a AirSqlTable) Partitions(*sql.Context) (sql.PartitionIter, error) { + return &slicePartitionIter{ + partitions: []sql.Partition{ + &memory.Partition{}, + }, + idx: 0, + }, nil +} + +func (a AirSqlTable) PartitionRows(ctx *sql.Context, _ sql.Partition) (sql.RowIter, error) { + recordsCfg := a.tableClient.GetRecords() + recs, err := recordsCfg.DoContext(ctx) + if err != nil { + return nil, merry.Wrap(err) + } + return &basicRowIter{ + recs: recs, + airSchema: a.airSchema, + idx: 0, + }, nil + +} + +type slicePartitionIter struct { + partitions []sql.Partition + idx int +} + +func (i *slicePartitionIter) Next(*sql.Context) (sql.Partition, error) { + if i.idx >= len(i.partitions) { + return nil, io.EOF + } + i.idx++ + return i.partitions[i.idx-1], nil +} +func (i *slicePartitionIter) Close(*sql.Context) error { + i.partitions = nil + return nil +} + +type basicRowIter struct { + recs *airtable.Records + airSchema *airtable.TableSchema + idx int +} + +// Dispose implements sql.Disposable +func (b *basicRowIter) Dispose() { + b.recs = nil +} + +func (b *basicRowIter) Next(*sql.Context) (sql.Row, error) { + if b.idx >= len(b.recs.Records) { + return nil, io.EOF + } + rec := b.recs.Records[b.idx].Fields + b.idx++ + + var values []any + for _, field := range b.airSchema.Fields { + values = append(values, rec[field.Name]) + } + return values, nil +} + +func (b *basicRowIter) Close(*sql.Context) error { + b.recs = nil + return nil +} + +var ( + _ sql.RowIter = (*basicRowIter)(nil) + _ sql.Disposable = (*basicRowIter)(nil) +) diff --git a/provider/provider.go b/provider/provider.go new file mode 100644 index 0000000..b900561 --- /dev/null +++ b/provider/provider.go @@ -0,0 +1,57 @@ +package provider + +import ( + "slices" + + "github.com/ansel1/merry" + "github.com/dolthub/go-mysql-server/sql" + "github.com/mehanizm/airtable" + "github.com/samber/lo/it" + "github.com/sclgo/airtable-sql/internal/errhelp" +) + +var _ sql.DatabaseProvider = (*AirProvider)(nil) + +type AirProvider struct { + client *airtable.Client +} + +func (a *AirProvider) Database(ctx *sql.Context, name string) (sql.Database, error) { + cfg := a.client.GetBases() + bases, err := cfg.DoContext(ctx) + if err != nil { + return nil, errhelp.Errorf("could list bases while looking for %s: %w", name, err) + } + id := "" + for _, b := range bases.Bases { + if b.Name == name { + id = b.ID + } + } + if id == "" { + return nil, merry.New("could not find base for " + name) + } + return NewAirDB(name, id, a.client), nil +} + +func (a *AirProvider) HasDatabase(ctx *sql.Context, name string) bool { + allDbs := a.AllDatabases(ctx) + return it.Contains(it.Map(slices.Values(allDbs), sql.Database.Name), name) +} + +func (a *AirProvider) AllDatabases(ctx *sql.Context) []sql.Database { + cfg := a.client.GetBases() + bases, err := cfg.DoContext(ctx) + if err != nil { + return nil + } + var dbs []sql.Database + for _, b := range bases.Bases { + dbs = append(dbs, NewAirDB(b.Name, b.ID, a.client)) + } + return dbs +} + +func New(client *airtable.Client) *AirProvider { + return &AirProvider{client: client} +} diff --git a/provider/provider_integ_test.go b/provider/provider_integ_test.go new file mode 100644 index 0000000..729ed13 --- /dev/null +++ b/provider/provider_integ_test.go @@ -0,0 +1,39 @@ +package provider_test + +import ( + "os" + "testing" + + sqle "github.com/dolthub/go-mysql-server" + "github.com/dolthub/go-mysql-server/memory" + "github.com/dolthub/go-mysql-server/sql" + "github.com/mehanizm/airtable" + "github.com/murfffi/gorich/helperr" + "github.com/murfffi/gorich/lang" + "github.com/sclgo/airtable-sql/internal/errhelp" + "github.com/sclgo/airtable-sql/provider" + "github.com/stretchr/testify/require" +) + +func TestProvider(t *testing.T) { + t.Run("happy", func(t *testing.T) { + apiKey := os.Getenv("AIRTABLE_API_KEY") + require.NotEmpty(t, apiKey) + pro := provider.New(airtable.NewClient(apiKey)) + engine := sqle.NewDefault(pro) + + session := memory.NewSession(sql.NewBaseSession(), pro) + ctx := sql.NewContext(t.Context(), sql.WithSession(session)) + ctx.SetCurrentDatabase("Order Assembly") + + schema, rows, flags, err := engine.Query(ctx, "SELECT `Part Name` FROM Parts") + require.NoError(t, err) + require.False(t, flags.DmlIsSet()) + require.GreaterOrEqual(t, schema.IndexOfColName("Part Name"), 0) + defer helperr.CloseQuietly(errhelp.FuncCloser(lang.Bind(rows.Close, ctx))) + row, err := rows.Next(ctx) + require.NoError(t, err) + t.Log("row:", row) + require.Equal(t, "Power Supply Unit", row[0]) + }) +}