11package main
22
33import (
4+ "encoding/csv"
45 "fmt"
56 "os"
67 "strings"
@@ -12,6 +13,7 @@ import (
1213 "github.com/slingdata-io/sling-cli/core/dbio"
1314 "github.com/slingdata-io/sling-cli/core/dbio/connection"
1415 "github.com/slingdata-io/sling-cli/core/dbio/database"
16+ "github.com/slingdata-io/sling-cli/core/dbio/iop"
1517 "github.com/slingdata-io/sling-cli/core/env"
1618 "github.com/slingdata-io/sling-cli/core/sling"
1719 "github.com/spf13/cast"
@@ -32,7 +34,8 @@ func processConns(c *g.CliSC) (ok bool, err error) {
3234
3335 ef := env .LoadSlingEnvFile ()
3436 ec := connection.EnvFileConns {EnvFile : & ef }
35- asJSON := os .Getenv ("SLING_OUTPUT" ) == "json"
37+ // resolved per-subcommand below; default falls back to SLING_OUTPUT
38+ var asJSON , asArrow , asCSV bool
3639
3740 entries := connection .GetLocalConns (true )
3841 defer connection .CloseAll ()
@@ -86,6 +89,27 @@ func processConns(c *g.CliSC) (ok bool, err error) {
8689 case "exec" :
8790 env .SetTelVal ("task" , g .Marshal (g .M ("type" , sling .ConnExec )))
8891
92+ var output string
93+ output , err = ResolveOutputFormat (c , "json" , "csv" , "arrow" )
94+ if err != nil {
95+ return ok , err
96+ }
97+ asJSON = output == "json"
98+ asCSV = output == "csv"
99+ asArrow = output == "arrow"
100+
101+ // --limit: default 100, "0" means no limit. String type so we can
102+ // distinguish "not provided" (use default) from explicit 0.
103+ limit := uint64 (100 )
104+ if raw := strings .TrimSpace (cast .ToString (c .Vals ["limit" ])); raw != "" {
105+ n , parseErr := cast .ToUint64E (raw )
106+ if parseErr != nil {
107+ return ok , g .Error ("invalid --limit %q; expected a non-negative integer" , raw )
108+ }
109+ limit = n
110+ }
111+ queryOpts := g .M ("limit" , limit )
112+
89113 name := cast .ToString (c .Vals ["name" ])
90114 conn := entries .Get (name )
91115 if conn .Name == "" {
@@ -126,22 +150,97 @@ func processConns(c *g.CliSC) (ok bool, err error) {
126150
127151 if len (database .ParseSQLMultiStatements (query )) == 1 && (! sQuery .IsQuery () || (strings .Contains (strings .ToLower (query ), "select" ) && ! strings .Contains (strings .ToLower (query ), "insert" )) || g .In (conn .Connection .Type , dbio .TypeDbPrometheus , dbio .TypeDbMongoDB , dbio .TypeDbElasticsearch )) {
128152
129- sql := sQuery .Select (database.SelectOptions {Limit : g .Ptr (100 )})
130- if sQuery .IsQuery () || sQuery .IsProcedural () {
131- sql = sQuery .Raw
153+ // Limit handling:
154+ // - limit > 0: wrap the SQL with the dialect's limit_sql template via
155+ // sQuery.Select(...) so the database truncates server-side. This
156+ // works for both bare tables and raw SELECT queries.
157+ // - limit == 0: unlimited; for raw queries fall back to sQuery.Raw so
158+ // we don't wrap with `LIMIT 0`.
159+ // - procedural calls (stored procs, etc.) cannot be wrapped, so they
160+ // always use sQuery.Raw and ignore the limit.
161+ var selectOpts database.SelectOptions
162+ if limit > 0 {
163+ n := int (limit )
164+ selectOpts .Limit = & n
132165 }
133- data , err := dbConn . Query ( sql )
134- if err != nil {
135- return ok , g . Error ( err , "cannot execute query" )
166+ sql := sQuery . Select ( selectOpts )
167+ if sQuery . IsProcedural () || ( limit == 0 && sQuery . IsQuery ()) {
168+ sql = sQuery . Raw
136169 }
137170
138- if asJSON {
139- fmt .Println (g .Marshal (g .M ("fields" , data .GetFields (), "rows" , data .Rows )))
171+ if asArrow || asCSV {
172+ // Streaming path: pull rows from StreamRowsContext and write them
173+ // directly to stdout (Arrow IPC stream or CSV). Logs go to stderr
174+ // so the output stays clean. Memory stays bounded for large queries.
175+ ds , err := dbConn .Self ().StreamRowsContext (dbConn .Context ().Ctx , sql , queryOpts )
176+ if err != nil {
177+ return ok , g .Error (err , "cannot execute query" )
178+ }
179+ if err := ds .WaitReady (); err != nil {
180+ return ok , g .Error (err , "datastream not ready" )
181+ }
182+
183+ var rowCount int64
184+ if asArrow {
185+ aw , err := iop .NewArrowStreamWriter (os .Stdout , ds .Columns )
186+ if err != nil {
187+ return ok , g .Error (err , "could not create arrow writer" )
188+ }
189+ for row := range ds .Rows () {
190+ if err := aw .WriteRow (row ); err != nil {
191+ aw .Close ()
192+ return ok , g .Error (err , "could not write arrow row" )
193+ }
194+ rowCount ++
195+ }
196+ if err := ds .Err (); err != nil {
197+ aw .Close ()
198+ return ok , g .Error (err , "error while streaming rows" )
199+ }
200+ if err := aw .Close (); err != nil {
201+ return ok , g .Error (err , "could not close arrow writer" )
202+ }
203+ } else { // asCSV
204+ w := csv .NewWriter (os .Stdout )
205+ if err := w .Write (ds .Columns .Names ()); err != nil {
206+ return ok , g .Error (err , "could not write csv header" )
207+ }
208+ rec := make ([]string , len (ds .Columns ))
209+ for row := range ds .Rows () {
210+ for i , val := range row {
211+ if i >= len (ds .Columns ) {
212+ break
213+ }
214+ rec [i ] = ds .Sp .CastToStringCSV (i , val , ds .Columns [i ].Type )
215+ }
216+ if err := w .Write (rec ); err != nil {
217+ return ok , g .Error (err , "could not write csv row" )
218+ }
219+ rowCount ++
220+ }
221+ w .Flush ()
222+ if err := w .Error (); err != nil {
223+ return ok , g .Error (err , "csv writer error" )
224+ }
225+ if err := ds .Err (); err != nil {
226+ return ok , g .Error (err , "error while streaming rows" )
227+ }
228+ }
229+ totalAffected = rowCount
140230 } else {
141- fmt .Println (g .PrettyTable (data .GetFields (), data .Rows ))
142- }
231+ data , err := dbConn .Query (sql , queryOpts )
232+ if err != nil {
233+ return ok , g .Error (err , "cannot execute query" )
234+ }
143235
144- totalAffected = cast .ToInt64 (len (data .Rows ))
236+ if asJSON {
237+ fmt .Println (g .Marshal (g .M ("fields" , data .GetFields (), "rows" , data .Rows )))
238+ } else {
239+ fmt .Println (g .PrettyTable (data .GetFields (), data .Rows ))
240+ }
241+
242+ totalAffected = cast .ToInt64 (len (data .Rows ))
243+ }
145244 } else {
146245 if len (queries ) > 1 {
147246 if strings .HasPrefix (query , "file://" ) {
@@ -176,7 +275,7 @@ func processConns(c *g.CliSC) (ok bool, err error) {
176275
177276 case "list" :
178277 fields , rows := entries .List ()
179- if asJSON {
278+ if os . Getenv ( "SLING_OUTPUT" ) == "json" {
180279 fmt .Println (g .Marshal (g .M ("fields" , fields , "rows" , rows )))
181280 } else {
182281 fmt .Println (g .PrettyTable (fields , rows ))
@@ -201,7 +300,7 @@ func processConns(c *g.CliSC) (ok bool, err error) {
201300 err = g .Error (err , "could not test %s" , name )
202301 }
203302
204- if asJSON {
303+ if os . Getenv ( "SLING_OUTPUT" ) == "json" {
205304 fmt .Println (g .Marshal (g .M ("success" , err == nil , "error" , g .ErrMsg (err ))))
206305 return
207306 }
@@ -222,3 +321,20 @@ func processConns(c *g.CliSC) (ok bool, err error) {
222321 }
223322 return ok , nil
224323}
324+
325+ // ResolveOutputFormat resolves the output format for `conns` subcommands.
326+ func ResolveOutputFormat (c * g.CliSC , allowed ... string ) (string , error ) {
327+ output := strings .ToLower (strings .TrimSpace (cast .ToString (c .Vals ["output" ])))
328+ if output == "" {
329+ output = strings .ToLower (strings .TrimSpace (os .Getenv ("SLING_OUTPUT" )))
330+ }
331+ if output == "" || output == "text" {
332+ return "" , nil
333+ }
334+ for _ , a := range allowed {
335+ if output == a {
336+ return output , nil
337+ }
338+ }
339+ return "" , g .Error ("invalid --output %q; expected one of: text, %s" , output , strings .Join (allowed , ", " ))
340+ }
0 commit comments