feat: jolt-side java.sql interop + defn/defmacro meta & arity-clause fixes

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.
This commit is contained in:
Yogthos 2026-06-13 15:28:54 -04:00
parent e2d33df484
commit 19505a5944
4 changed files with 78 additions and 6 deletions

View file

@ -341,10 +341,14 @@
(defmacro defn [fn-name & body] (defmacro defn [fn-name & body]
(let [body (if (and (seq body) (string? (first body))) (rest body) 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)))) 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, ;; 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) ;; 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 ;; Jolt doesn't enforce privacy, so defn- is just defn (matching how Clojure's own
;; defn- delegates to defn with :private metadata). ;; defn- delegates to defn with :private metadata).

View file

@ -1724,6 +1724,26 @@
# the __write / __pr-str1 host seams; str-render-one stays for core-str. # the __write / __pr-str1 host seams; str-render-one stays for core-str.
(defn core-write [s] (prin s) nil) (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). # newline lives in the Clojure collection tier (core/20-coll.clj).
# Clojure 1.11 string->scalar parsers: nil on malformed input, throw on a # Clojure 1.11 string->scalar parsers: nil on malformed input, throw on a
@ -2706,6 +2726,11 @@
"hash-unordered-coll" core-hash-unordered-coll "hash-unordered-coll" core-hash-unordered-coll
"gensym" gensym "gensym" gensym
"__write" core-write "__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 "__pr-str1" core-pr-str1
"__make-uuid" make-uuid "__make-uuid" make-uuid
"compare" core-compare "compare" core-compare

View file

@ -1583,6 +1583,11 @@
"DateTimeFormatter" (and (table? val) (= :jolt/dt-formatter (get val :jolt/type))) "DateTimeFormatter" (and (table? val) (= :jolt/dt-formatter (get val :jolt/type)))
"URL" (and (table? val) (= :jolt/url (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))) "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 # JVM char[] class — (Class/forName "[C"); jolt char arrays are Janet
# arrays of char structs # arrays of char structs
"[C" (and (array? val) "[C" (and (array? val)
@ -1793,12 +1798,23 @@
(put v :dynamic true)) (put v :dynamic true))
# def returns the var (Clojure semantics); REPL prints #'ns/name # def returns the var (Clojure semantics); REPL prints #'ns/name
v))) 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) rest-form (tuple/slice form 2)
# optional docstring # optional docstring
has-doc? (and (> (length rest-form) 0) (string? (first rest-form))) has-doc? (and (> (length rest-form) 0) (string? (first rest-form)))
args-form (if has-doc? (in rest-form 1) (first rest-form)) raw-args (if has-doc? (in rest-form 1) (first rest-form))
body (tuple/slice rest-form (if has-doc? 2 1)) 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) param-info (parse-params args-form)
fixed-pats (param-info :fixed) fixed-pats (param-info :fixed)
rest-pat (param-info :rest) rest-pat (param-info :rest)

View file

@ -536,7 +536,34 @@
# Thread stub: getContextClassLoader returns a stub so migratus jar/create code # Thread stub: getContextClassLoader returns a stub so migratus jar/create code
# that walks Thread/currentThread doesn't crash. # that walks Thread/currentThread doesn't crash.
(register-tagged-methods! :jolt/thread (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!)
(install-io!) (install-io!)