pizzasql.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package pizzasql
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. )
  11. // Client represents a connection to a PizzaSQL database
  12. type Client struct {
  13. baseURL string
  14. dbName string
  15. apiKey string
  16. client *http.Client
  17. }
  18. // Row represents a single row in the result set
  19. type Row map[string]interface{}
  20. // QueryResult represents the result of a SQL query
  21. type QueryResult struct {
  22. Rows []Row `json:"rows"`
  23. }
  24. // Connect creates a new PizzaSQL client connection
  25. // URI format: http://host:port/dbname or https://pizzabase.cloud/my_org/sql/my_db:32131
  26. func Connect(uri string, apiKey string) (*Client, error) {
  27. parsedURL, err := url.Parse(uri)
  28. if err != nil {
  29. return nil, fmt.Errorf("invalid URI: %w", err)
  30. }
  31. // Extract database name from path
  32. path := strings.Trim(parsedURL.Path, "/")
  33. if path == "" {
  34. return nil, fmt.Errorf("database name not found in URI path")
  35. }
  36. // Split path to get database name (last segment)
  37. pathParts := strings.Split(path, "/")
  38. dbName := pathParts[len(pathParts)-1]
  39. // Reconstruct base URL without the database path
  40. baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
  41. return &Client{
  42. baseURL: baseURL,
  43. dbName: dbName,
  44. apiKey: apiKey,
  45. client: &http.Client{},
  46. }, nil
  47. }
  48. // SQL executes a SQL query and returns the results as a slice of rows
  49. func (c *Client) SQL(query string) ([]Row, error) {
  50. // Prepare request body
  51. body := map[string]string{"query": query}
  52. jsonBody, err := json.Marshal(body)
  53. if err != nil {
  54. return nil, fmt.Errorf("failed to marshal request: %w", err)
  55. }
  56. // Create request
  57. url := fmt.Sprintf("%s/%s/query", c.baseURL, c.dbName)
  58. req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
  59. if err != nil {
  60. return nil, fmt.Errorf("failed to create request: %w", err)
  61. }
  62. // Set headers
  63. req.Header.Set("Content-Type", "application/json")
  64. if c.apiKey != "" {
  65. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  66. }
  67. // Execute request
  68. resp, err := c.client.Do(req)
  69. if err != nil {
  70. return nil, fmt.Errorf("request failed: %w", err)
  71. }
  72. defer resp.Body.Close()
  73. // Read response body
  74. respBody, err := io.ReadAll(resp.Body)
  75. if err != nil {
  76. return nil, fmt.Errorf("failed to read response: %w", err)
  77. }
  78. // Check status code
  79. if resp.StatusCode != http.StatusOK {
  80. return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respBody))
  81. }
  82. // Parse response
  83. var result QueryResult
  84. if err := json.Unmarshal(respBody, &result); err != nil {
  85. return nil, fmt.Errorf("failed to parse response: %w", err)
  86. }
  87. return result.Rows, nil
  88. }
  89. // Export exports a database or table to SQL or CSV format
  90. func (c *Client) Export(table string, format string) ([]byte, error) {
  91. params := url.Values{}
  92. if table != "" {
  93. params.Set("table", table)
  94. }
  95. if format != "" {
  96. params.Set("format", format)
  97. }
  98. url := fmt.Sprintf("%s/%s/export?%s", c.baseURL, c.dbName, params.Encode())
  99. req, err := http.NewRequest("GET", url, nil)
  100. if err != nil {
  101. return nil, fmt.Errorf("failed to create request: %w", err)
  102. }
  103. if c.apiKey != "" {
  104. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  105. }
  106. resp, err := c.client.Do(req)
  107. if err != nil {
  108. return nil, fmt.Errorf("request failed: %w", err)
  109. }
  110. defer resp.Body.Close()
  111. data, err := io.ReadAll(resp.Body)
  112. if err != nil {
  113. return nil, fmt.Errorf("failed to read response: %w", err)
  114. }
  115. if resp.StatusCode != http.StatusOK {
  116. return nil, fmt.Errorf("export failed with status %d: %s", resp.StatusCode, string(data))
  117. }
  118. return data, nil
  119. }
  120. // Import imports data from SQL or CSV format
  121. func (c *Client) Import(data []byte, format string, createTable bool) error {
  122. params := url.Values{}
  123. if format != "" {
  124. params.Set("format", format)
  125. }
  126. if createTable {
  127. params.Set("create_table", "true")
  128. }
  129. url := fmt.Sprintf("%s/%s/import?%s", c.baseURL, c.dbName, params.Encode())
  130. req, err := http.NewRequest("POST", url, bytes.NewReader(data))
  131. if err != nil {
  132. return fmt.Errorf("failed to create request: %w", err)
  133. }
  134. req.Header.Set("Content-Type", "application/octet-stream")
  135. if c.apiKey != "" {
  136. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  137. }
  138. resp, err := c.client.Do(req)
  139. if err != nil {
  140. return fmt.Errorf("request failed: %w", err)
  141. }
  142. defer resp.Body.Close()
  143. respBody, err := io.ReadAll(resp.Body)
  144. if err != nil {
  145. return fmt.Errorf("failed to read response: %w", err)
  146. }
  147. if resp.StatusCode != http.StatusOK {
  148. return fmt.Errorf("import failed with status %d: %s", resp.StatusCode, string(respBody))
  149. }
  150. return nil
  151. }