From 19505a5944e43465825ad2971272c7107eba5d81 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 13 Jun 2026 15:28:54 -0400 Subject: [PATCH] feat: jolt-side java.sql interop + defn/defmacro meta & arity-clause fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the migratus next.jdbc shim (jolt-0z5): - core.janet: __jdbc-wrap-conn / __jdbc-conn-raw / __jdbc-make-stmt builtins. A connection is a tagged wrapper over a jdbc.core conn carrying a clj :exec callback so the host Statement.executeBatch runs SQL without a janet->clj call. - javatime.janet: tagged-methods for :jolt/jdbc-conn (setAutoCommit/isClosed/ close/getMetaData), :jolt/jdbc-meta (getDatabaseProductName), :jolt/jdbc-stmt (addBatch/executeBatch/close); java.sql.Timestamp ctor -> millis. - evaluator.janet: instance? case for Connection/java.sql.Connection so migratus's do-commands runs SQL through its Connection branch. Two defn/defmacro fixes found loading migratus.core (both rooted in the reader representing ^{:map} name metadata as a with-meta form, jolt-8w2): - defmacro special form: unwrap a with-meta name (mirrors def), and handle the arity-clause form (defmacro name ([params] body...)) like fn/defn — a params vector reads as a tuple, an arity clause as a list (array). - defn overlay: pass the bare (unwrapped) name to fn while def keeps the meta. Conformance 335/335 x3 modes. --- jolt-core/clojure/core/00-syntax.clj | 8 ++++++-- src/jolt/core.janet | 25 ++++++++++++++++++++++++ src/jolt/evaluator.janet | 22 ++++++++++++++++++--- src/jolt/javatime.janet | 29 +++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/jolt-core/clojure/core/00-syntax.clj b/jolt-core/clojure/core/00-syntax.clj index a2ce11e..17238a8 100644 --- a/jolt-core/clojure/core/00-syntax.clj +++ b/jolt-core/clojure/core/00-syntax.clj @@ -341,10 +341,14 @@ (defmacro defn [fn-name & body] (let [body (if (and (seq body) (string? (first body))) (rest body) body) body (if (and (seq body) (map? (first body)) (not (symbol? (first body)))) - (rest body) body)] + (rest body) body) + ;; ^{:map} metadata on the name reads as a (with-meta sym …) form, not an + ;; annotated symbol (jolt-8w2). def attaches the metadata, but fn needs a + ;; bare symbol, so unwrap it for the fn name. + fn-only-name (if (symbol? fn-name) fn-name (first (rest fn-name)))] ;; pass the name through to fn: the compiled fn's janet name carries it, ;; so stack traces read app.deep/level3 instead of a gensym (jolt-2o7.1) - `(def ~fn-name (fn ~fn-name ~@body)))) + `(def ~fn-name (fn ~fn-only-name ~@body)))) ;; Jolt doesn't enforce privacy, so defn- is just defn (matching how Clojure's own ;; defn- delegates to defn with :private metadata). diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 15d5ae6..039a37c 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -1724,6 +1724,26 @@ # the __write / __pr-str1 host seams; str-render-one stays for core-str. (defn core-write [s] (prin s) nil) +(defn core-eprint [s] (eprint s) nil) +(defn core-eprintf [fmt & args] (eprintf fmt ;args) nil) + +# next.jdbc host shims (the db library's next.jdbc compat layer builds on these). +# A connection is a tagged table wrapping a jdbc.core conn (:raw) plus clj +# callbacks: :exec runs one SQL string in the transaction (so the janet-side +# Statement.executeBatch can run SQL without a janet->clj call), :close closes +# the underlying conn, :product is the JDBC database product name. instance? +# Connection (evaluator) and the :jolt/jdbc-conn tagged methods (javatime) key +# off this shape. +(defn core-jdbc-wrap-conn [raw exec closef product] + @{:jolt/type :jolt/jdbc-conn :raw raw :exec exec :close closef + :product product :closed @[false]}) +# Robust unwrap: a wrapped conn -> its :raw; anything else passes through (so the +# next.jdbc fns accept either a wrapped conn or a bare jdbc.core conn/spec). +(defn core-jdbc-conn-raw [x] + (if (and (table? x) (= :jolt/jdbc-conn (get x :jolt/type))) (get x :raw) x)) +(defn core-jdbc-make-stmt [w] + @{:jolt/type :jolt/jdbc-stmt :exec (get w :exec) :cmds @[]}) + # newline lives in the Clojure collection tier (core/20-coll.clj). # Clojure 1.11 string->scalar parsers: nil on malformed input, throw on a @@ -2706,6 +2726,11 @@ "hash-unordered-coll" core-hash-unordered-coll "gensym" gensym "__write" core-write + "__eprint" core-eprint + "__eprintf" core-eprintf + "__jdbc-wrap-conn" core-jdbc-wrap-conn + "__jdbc-conn-raw" core-jdbc-conn-raw + "__jdbc-make-stmt" core-jdbc-make-stmt "__pr-str1" core-pr-str1 "__make-uuid" make-uuid "compare" core-compare diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 5977da5..1c654ac 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -1583,6 +1583,11 @@ "DateTimeFormatter" (and (table? val) (= :jolt/dt-formatter (get val :jolt/type))) "URL" (and (table? val) (= :jolt/url (get val :jolt/type))) "java.net.URL" (and (table? val) (= :jolt/url (get val :jolt/type))) + # next.jdbc host shim: a wrapped jdbc.core connection (core.janet). + # migratus's do-commands only runs SQL through its (instance? Connection) + # branch, so the wrapped conn must answer true here. + "Connection" (and (table? val) (= :jolt/jdbc-conn (get val :jolt/type))) + "java.sql.Connection" (and (table? val) (= :jolt/jdbc-conn (get val :jolt/type))) # JVM char[] class — (Class/forName "[C"); jolt char arrays are Janet # arrays of char structs "[C" (and (array? val) @@ -1793,12 +1798,23 @@ (put v :dynamic true)) # def returns the var (Clojure semantics); REPL prints #'ns/name v))) - "defmacro" (let [name-sym (in form 1) + "defmacro" (let [# ^{:map} metadata on the name reads as a (with-meta sym …) + # form (jolt-8w2); unwrap to the bare symbol like def does. + name-sym (unwrap-meta-name (in form 1)) rest-form (tuple/slice form 2) # optional docstring has-doc? (and (> (length rest-form) 0) (string? (first rest-form))) - args-form (if has-doc? (in rest-form 1) (first rest-form)) - body (tuple/slice rest-form (if has-doc? 2 1)) + raw-args (if has-doc? (in rest-form 1) (first rest-form)) + raw-body (tuple/slice rest-form (if has-doc? 2 1)) + # arity-clause form: (defmacro name ([params] body...)) — like + # fn/defn. In a code form a vector literal is a tuple and a list + # is an array, so a params vector reads as a tuple while an arity + # clause reads as a list (array): unwrap its params + body. Single + # arity only (what real macros use); a multi-arity macro takes the + # first clause. + arity-clause? (array? raw-args) + args-form (if arity-clause? (first raw-args) raw-args) + body (if arity-clause? (tuple/slice raw-args 1) raw-body) param-info (parse-params args-form) fixed-pats (param-info :fixed) rest-pat (param-info :rest) diff --git a/src/jolt/javatime.janet b/src/jolt/javatime.janet index 66c7791..457de78 100644 --- a/src/jolt/javatime.janet +++ b/src/jolt/javatime.janet @@ -536,7 +536,34 @@ # Thread stub: getContextClassLoader returns a stub so migratus jar/create code # that walks Thread/currentThread doesn't crash. (register-tagged-methods! :jolt/thread - @{"getContextClassLoader" (fn [self] @{:jolt/type :jolt/classloader})})) + @{"getContextClassLoader" (fn [self] @{:jolt/type :jolt/classloader})}) + # next.jdbc host shims (paired with the __jdbc-* builtins in core.janet and the + # instance? Connection case in evaluator.janet). The wrapped connection carries + # a clj :exec callback (run one SQL string) and a :close callback. + # java.sql.Timestamp: migratus builds (Timestamp. millis) for the applied + # column; represent it as the millis number so it stores and sorts directly. + (each nm ["Timestamp" "java.sql.Timestamp"] + (register-class-ctor! nm (fn [ms] ms))) + (register-tagged-methods! :jolt/jdbc-conn + @{"setAutoCommit" (fn [self _] self) + "isClosed" (fn [self] (get (get self :closed) 0)) + "close" (fn [self] + (unless (get (get self :closed) 0) + ((get self :close)) + (put (get self :closed) 0 true)) + nil) + "getMetaData" (fn [self] @{:jolt/type :jolt/jdbc-meta :product (get self :product)})}) + (register-tagged-methods! :jolt/jdbc-meta + @{"getDatabaseProductName" (fn [self] (get self :product))}) + # Statement batch: addBatch accumulates SQL strings, executeBatch runs each via + # the connection's clj :exec callback (which executes inside the transaction). + (register-tagged-methods! :jolt/jdbc-stmt + @{"addBatch" (fn [self sql] (array/push (get self :cmds) sql) nil) + "executeBatch" (fn [self] + (def out @[]) + (each c (get self :cmds) (array/push out ((get self :exec) c))) + out) + "close" (fn [self] nil)})) (install!) (install-io!)