Skip to content

Commit

Permalink
Adding ScanAllSets to the api (#115)
Browse files Browse the repository at this point in the history
* feat: add ScanAllSets

* test: mssql for ScanAllSets

* fix: ScanOne should close rows

* Add a way to test mssql

* fix linters

* Fix godot

---------

Co-authored-by: Georgy Savva <georgy.savva@gmail.com>
  • Loading branch information
kmpm and georgysavva committed Jan 6, 2024
1 parent 80c2cc6 commit d60e58c
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 14 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ jobs:
env:
GO: ${{ matrix.go-version }}
runs-on: ubuntu-latest
services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
env:
SA_PASSWORD: p@sSword
ACCEPT_EULA: "Y"
ports:
- 1433:1433
steps:
- name: Download CockroachDB Binary
run: |
Expand Down Expand Up @@ -45,7 +53,7 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}

- name: Test
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... --cockroach-binary cockroach
run: go test --tags with_mssql -v -race -coverprofile=coverage.txt -covermode=atomic ./... --cockroach-binary cockroach

- name: Upload Codecov
uses: codecov/codecov-action@v3
Expand Down
2 changes: 0 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ linters-settings:
- whyNoLint
gocyclo:
min-complexity: 15
godot:
check-all: true
goimports:
local-prefixes: github.com/georgysavva/scany
gomnd:
Expand Down
39 changes: 32 additions & 7 deletions dbscan/dbscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Rows interface {
Next() bool
Columns() ([]string, error)
Scan(dest ...interface{}) error
NextResultSet() bool
}

// ScanAll is a package-level helper function that uses the DefaultAPI object.
Expand All @@ -30,6 +31,12 @@ func ScanOne(dst interface{}, rows Rows) error {
return DefaultAPI.ScanOne(dst, rows)
}

// ScanAllSets is a package-level helper function that uses the DefaultAPI object.
// See API.ScanAllSets for details.
func ScanAllSets(dsts []interface{}, rows Rows) error {
return DefaultAPI.ScanAllSets(dsts, rows)
}

// NameMapperFunc is a function type that maps a struct field name to the database column name.
type NameMapperFunc func(string) string

Expand Down Expand Up @@ -162,7 +169,7 @@ func WithAllowUnknownColumns(allowUnknownColumns bool) APIOption {
// Before starting, ScanAll resets the destination slice,
// so if it's not empty it will overwrite all existing elements.
func (api *API) ScanAll(dst interface{}, rows Rows) error {
return api.processRows(dst, rows, true /* multipleRows. */)
return api.processRows(dst, rows, true /* multipleRows. */, true /* closeRows. */)
}

// ScanOne iterates all rows to the end and makes sure that there was exactly one row
Expand All @@ -171,7 +178,22 @@ func (api *API) ScanAll(dst interface{}, rows Rows) error {
// and propagates any errors that could pop up.
// It scans data from that single row into the destination.
func (api *API) ScanOne(dst interface{}, rows Rows) error {
return api.processRows(dst, rows, false /* multipleRows. */)
return api.processRows(dst, rows, false /* multipleRows. */, true /* closeRows. */)
}

// ScanAllSets iterates all rows to the end and scans data into each destination.
// Multiple destinations is supported by multiple result sets.
func (api *API) ScanAllSets(dsts []interface{}, rows Rows) error {
defer rows.Close() //nolint: errcheck
for i, dst := range dsts {
if err := api.processRows(dst, rows, true, false /* closeRows */); err != nil {
return fmt.Errorf("error processing destination %d: %w", i, err)
}
if !rows.NextResultSet() {
break
}
}
return nil
}

// NotFound returns true if err is a not found error.
Expand All @@ -189,8 +211,10 @@ type sliceDestinationMeta struct {
elementByPtr bool
}

func (api *API) processRows(dst interface{}, rows Rows, multipleRows bool) error {
defer rows.Close() //nolint: errcheck
func (api *API) processRows(dst interface{}, rows Rows, multipleRows, closeRows bool) error {
if closeRows {
defer rows.Close() //nolint: errcheck
}
var sliceMeta *sliceDestinationMeta
if multipleRows {
var err error
Expand Down Expand Up @@ -219,9 +243,10 @@ func (api *API) processRows(dst interface{}, rows Rows, multipleRows bool) error
if err := rows.Err(); err != nil {
return fmt.Errorf("scany: rows final error: %w", err)
}

if err := rows.Close(); err != nil {
return fmt.Errorf("scany: close rows after processing: %w", err)
if closeRows {
if err := rows.Close(); err != nil {
return fmt.Errorf("scany: close rows after processing: %w", err)
}
}

exactlyOneRow := !multipleRows
Expand Down
1 change: 1 addition & 0 deletions dbscan/rowscanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ func (er emptyRow) Next() bool { return true }
func (er emptyRow) Columns() ([]string, error) { return []string{}, nil }
func (er emptyRow) Close() error { return nil }
func (er emptyRow) Err() error { return nil }
func (er emptyRow) NextResultSet() bool { return false }

func TestRowScanner_Scan_primitiveTypeDestinationRowsContainZeroColumns_returnsErr(t *testing.T) {
t.Parallel()
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: "3.2"
services:
sql-server-db:
image: mcr.microsoft.com/mssql/server:2017-latest
ports:
- "1433:1433"
environment:
SA_PASSWORD: "p@sSword"
ACCEPT_EULA: "Y"
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ go 1.18
require (
github.com/cockroachdb/cockroach-go/v2 v2.2.0
github.com/jackc/pgx/v5 v5.0.0
github.com/stretchr/testify v1.8.0
github.com/microsoft/go-mssqldb v1.6.0
github.com/stretchr/testify v1.8.4
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/puddle/v2 v2.0.0 // indirect
github.com/lib/pq v1.10.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
23 changes: 21 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
Expand All @@ -13,7 +19,13 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
Expand Down Expand Up @@ -73,6 +85,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
Expand All @@ -88,6 +101,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc=
github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
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=
Expand All @@ -108,15 +124,17 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
Expand All @@ -143,6 +161,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
6 changes: 6 additions & 0 deletions pgxscan/pgxscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ func (ra RowsAdapter) Close() error {
return nil
}

// NextResultSet is currently always returning false.
func (ra RowsAdapter) NextResultSet() bool {
// TODO: when pgx issue #308 and #1512 and is fixed mabye we can do something here.
return false
}

func mustNewDBScanAPI(opts ...dbscan.APIOption) *dbscan.API {
api, err := NewDBScanAPI(opts...)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions sqlscan/sqlscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ func ScanOne(dst interface{}, rows *sql.Rows) error {
return DefaultAPI.ScanOne(dst, rows)
}

// ScanAllSets is a package-level helper function that uses the DefaultAPI object.
// See API.ScanAllSets for details.
func ScanAllSets(dsts []interface{}, rows *sql.Rows) error {
return DefaultAPI.ScanAllSets(dsts, rows)
}

// RowScanner is a wrapper around the dbscan.RowScanner type.
// See dbscan.RowScanner for details.
type RowScanner struct {
Expand Down Expand Up @@ -133,6 +139,12 @@ func (api *API) ScanOne(dst interface{}, rows *sql.Rows) error {
}
}

// ScanAllSets is a wrapper around the dbscan.ScanAllSets function.
// See dbscan.ScanAllSets for details.
func (api *API) ScanAllSets(dsts []interface{}, rows *sql.Rows) error {
return api.dbscanAPI.ScanAllSets(dsts, rows)
}

// NotFound is a helper function to check if an error
// is `sql.ErrNoRows`.
func NotFound(err error) bool {
Expand Down
78 changes: 78 additions & 0 deletions sqlscan/sqlscan_ms_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//go:build with_mssql
// +build with_mssql

package sqlscan_test

import (
"database/sql"
"fmt"
"os"
"testing"

"github.com/georgysavva/scany/v2/sqlscan"
_ "github.com/microsoft/go-mssqldb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
multipleSetsQueryMssql = `
SELECT *
FROM (
VALUES ('foo val', 'bar val'), ('foo val 2', 'bar val 2'), ('foo val 3', 'bar val 3')
) as t1 (foo, bar);
SELECT *
FROM (
VALUES ('egg val', 'bacon val')
) as t2 (egg, bacon);
`
)

func getEnv(key string, defaultValue string) string {
value, exists := os.LookupEnv(key)
if !exists {
return defaultValue
}
return value
}
func TestMSScanAllSets(t *testing.T) {
t.Parallel()
testSqliteDB, err := sql.Open("sqlserver", getEnv("MSSQL_URL", "sqlserver://sa:p@sSword@localhost:1433?database=master"))
if err != nil {
require.NoError(t, err)
}
dbscanAPI, err := sqlscan.NewDBScanAPI()
if err != nil {
require.NoError(t, fmt.Errorf("new DB scan API: %w", err))
}
api, err := sqlscan.NewAPI(dbscanAPI)
if err != nil {
require.NoError(t, fmt.Errorf("new API: %w", err))
}
type testModel2 struct {
Egg string
Bacon string
}

expected1 := []*testModel{
{Foo: "foo val", Bar: "bar val"},
{Foo: "foo val 2", Bar: "bar val 2"},
{Foo: "foo val 3", Bar: "bar val 3"},
}
expected2 := []*testModel2{
{Egg: "egg val", Bacon: "bacon val"},
}

rows, err := testSqliteDB.Query(multipleSetsQueryMssql)
require.NoError(t, err)

var got1 []*testModel
var got2 []*testModel2
err = api.ScanAllSets([]any{&got1, &got2}, rows)
require.NoError(t, err)

assert.Equal(t, expected1, got1)
assert.Equal(t, expected2, got2)
}
28 changes: 28 additions & 0 deletions sqlscan/sqlscan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ func TestScanOne_noRows_returnsNotFoundErr(t *testing.T) {
assert.True(t, errors.Is(err, sql.ErrNoRows))
}

func TestScanAllSets(t *testing.T) {
t.Parallel()

type testModel2 struct {
Egg string
Bacon string
}

expected1 := []*testModel{
{Foo: "foo val", Bar: "bar val"},
{Foo: "foo val 2", Bar: "bar val 2"},
{Foo: "foo val 3", Bar: "bar val 3"},
}

rows, err := testDB.Query(multipleRowsQuery)
require.NoError(t, err)

var got1 []*testModel
var got2 []*testModel2

err = testAPI.ScanAllSets([]any{&got1, &got2}, rows)
require.NoError(t, err)

assert.Equal(t, expected1, got1)
// PG does not support multiple result sets. Only the first result set is returned.
assert.Nil(t, got2)
}

func TestRowScanner_Scan(t *testing.T) {
t.Parallel()
rows, err := testDB.Query(singleRowsQuery)
Expand Down

0 comments on commit d60e58c

Please sign in to comment.