Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ When adding new features, follow this order:

---

## Formatting

Run the formatter before committing:

```
clj -M:fmt fix
```

---

## Version Synchronization

**Important**: `src/pine/version.clj` must match `playground.docker-compose.yml` Docker image version.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).

## [Unreleased]

## [0.35.0] - 2026-05-05
### Added
- Per-session database connections: `build`, `eval`, and `sql` endpoints now accept an optional `connection-id` parameter. Queries run against that specific connection pool; when absent, the global connection is used (backward compatible).

## [0.34.0] - 2026-05-04
### Added
- Update partial column hints: `u!` can be followed by an incomplete column token (for example `company | u! i`), parsed like `where` partials and suggesting matching assignable columns. After completed assignments, `u! id = '1', col` supports partial completion for the next column name.
Expand Down
2 changes: 1 addition & 1 deletion playground.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:
retries: 10

pine:
image: ahmadnazir/pine:0.34.0
image: ahmadnazir/pine:0.35.0
container_name: pine_server
depends_on:
sample-db-ecommerce:
Expand Down
137 changes: 74 additions & 63 deletions src/pine/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@

(defn- generate-state
([expression]
(generate-state expression nil))
(generate-state expression nil nil))
([expression cursor]
(let [{:keys [result error]} (->> expression parser/parse)]
(if result {:result (ast/generate result @db/connection-id expression cursor)}
(generate-state expression cursor nil))
([expression cursor connection-id]
(let [{:keys [result error]} (->> expression parser/parse)
conn-id (or connection-id @db/connection-id)]
(if result {:result (ast/generate result conn-id expression cursor)}
{:error-type "parse"
:error error}))))

Expand All @@ -43,16 +46,19 @@

(defn api-build
([expression]
(api-build expression nil))
(api-build expression nil nil))
([expression cursor]
(let [connection-name (connections/get-connection-name @db/connection-id)]
(api-build expression cursor nil))
([expression cursor connection-id]
(let [conn-id (or connection-id @db/connection-id)
connection-name (connections/get-connection-name conn-id)]
(try
(let [result (generate-state expression cursor)
(let [result (generate-state expression cursor conn-id)
{state :result error :error} result]
(if error result
{:connection-id connection-name
:version version
:query (-> expression trim-pipes generate-state :result eval/build-query eval/formatted-query)
:query (-> expression trim-pipes (generate-state nil conn-id) :result eval/build-query eval/formatted-query)
:ast (select-keys state [:hints :selected-tables :joins :context :current :operation :columns :order :where :prettified :ranges])}))
(catch Exception e {:connection-id connection-name
:error (.getMessage e)})))))
Expand All @@ -75,27 +81,31 @@
(when-let [alias (state :alias)]
[alias])))))

(defn api-eval [expression]
(let [connection-name (connections/get-connection-name @db/connection-id)]
(try
(let [result (generate-state expression)
{state :result error :error} result]
(if error result
(let [rows (eval/run-query state)
op-type (get-in state [:operation :type])
;; For action results we control the format; columns come from header row
columns (if (contains? #{:update-action :delete-action} op-type)
(get-columns rows)
(get-columns state rows))]
{:connection-id connection-name
:version version
;; :time (db/run-query (state :connection-id) {:query "SELECT NOW() as now, NOW() AT TIME ZONE 'UTC' AS utc;"})
;; :server_time (str (java.time.Instant/now))
:result rows
:columns columns})))

(catch Exception e {:connection-id connection-name
:error (.getMessage e)}))))
(defn api-eval
([expression]
(api-eval expression nil))
([expression connection-id]
(let [conn-id (or connection-id @db/connection-id)
connection-name (connections/get-connection-name conn-id)]
(try
(let [result (generate-state expression nil conn-id)
{state :result error :error} result]
(if error result
(let [rows (eval/run-query state)
op-type (get-in state [:operation :type])
;; For action results we control the format; columns come from header row
columns (if (contains? #{:update-action :delete-action} op-type)
(get-columns rows)
(get-columns state rows))]
{:connection-id connection-name
:version version
;; :time (db/run-query (state :connection-id) {:query "SELECT NOW() as now, NOW() AT TIME ZONE 'UTC' AS utc;"})
;; :server_time (str (java.time.Instant/now))
:result rows
:columns columns})))

(catch Exception e {:connection-id connection-name
:error (.getMessage e)})))))

(defn get-connection []
(let [connection-id @db/connection-id]
Expand All @@ -122,36 +132,31 @@
(-> id test-connection :connection-id set-connection-pool)
(catch Exception e {:error (.getMessage e)})))

(defn api-sql [sql-query]
"Execute raw SQL query directly without pine expression evaluation.

Usage via HTTP POST to /api/v1/sql:
- For SELECT queries: Returns result set with rows
- For INSERT/UPDATE/DELETE: Returns affected row count

Example requests:
POST /api/v1/sql with body: {\"query\": \"SELECT * FROM users LIMIT 10\"}
POST /api/v1/sql with body: {\"query\": \"UPDATE users SET active = true WHERE id = 1\"}"
(let [connection-name (connections/get-connection-name @db/connection-id)]
(cond
(nil? sql-query)
{:connection-id connection-name
:error "SQL query is required. Please provide a 'query' parameter in the request body."}

(clojure.string/blank? sql-query)
{:connection-id connection-name
:error "SQL query cannot be empty."}

:else
(try
(let [result (db/run-sql @db/connection-id sql-query)
columns (when (vector? result) (get-columns result))]
{:connection-id connection-name
:version version
:result result
:columns columns})
(catch Exception e {:connection-id connection-name
:error (.getMessage e)})))))
(defn api-sql
([sql-query]
(api-sql sql-query nil))
([sql-query connection-id]
(let [conn-id (or connection-id @db/connection-id)
connection-name (connections/get-connection-name conn-id)]
(cond
(nil? sql-query)
{:connection-id connection-name
:error "SQL query is required. Please provide a 'query' parameter in the request body."}

(clojure.string/blank? sql-query)
{:connection-id connection-name
:error "SQL query cannot be empty."}

:else
(try
(let [result (db/run-sql conn-id sql-query)
columns (when (vector? result) (get-columns result))]
{:connection-id connection-name
:version version
:result result
:columns columns})
(catch Exception e {:connection-id connection-name
:error (.getMessage e)}))))))

(defn wrap-logger
[handler]
Expand All @@ -178,18 +183,24 @@
:time (str (java.time.LocalDateTime/now))} response))

;; query building and evaluation
(POST "/api/v1/build" [expression cursor] (->> (api-build expression cursor) response))
(POST "/api/v1/eval" [expression] (->> expression trim-pipes api-eval response))
(POST "/api/v1/build" {params :params}
(let [{:keys [expression cursor connection-id]} params]
(->> (api-build expression cursor connection-id) response)))
(POST "/api/v1/eval" {params :params}
(let [{:keys [expression connection-id]} params]
(->> (api-eval (trim-pipes expression) connection-id) response)))

;; raw SQL execution
(POST "/api/v1/sql" {params :params}
(let [query (:query params)]
(->> query api-sql response)))
(let [{:keys [query connection-id]} params]
(->> (api-sql query connection-id) response)))

;; Legacy
;;
;; pine-mode.el
(POST "/api/v1/build-with-params" [expression] (->> expression trim-pipes api-build :query response))
(POST "/api/v1/build-with-params" {params :params}
(let [{:keys [expression connection-id]} params]
(->> (api-build (trim-pipes expression) nil connection-id) :query response)))
;; default case
(route/not-found "Not Found"))
(def app
Expand Down
2 changes: 1 addition & 1 deletion src/pine/version.clj
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(ns pine.version)

(def version "0.34.0")
(def version "0.35.0")
Loading