2024年12月19日木曜日

Spring Data JPA の JPQL エラー備忘録(LocalDate がエラーになる)

Q: 以下エラーの原因がわかりますでしょうか?

Spring Data REST で、以下のコードを実装しました。

package dev.mikoto2000.study.springboot.web.practice20241215.repository;

import java.time.LocalDate;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import dev.mikoto2000.study.springboot.web.practice20241215.entity.BookMaster;
import dev.mikoto2000.study.springboot.web.practice20241215.projection.DefaultBookMasterProjection;

/**
 * BookMasterRepository
 */
@RepositoryRestResource(excerptProjection = DefaultBookMasterProjection.class)
public interface BookMasterRepository extends PagingAndSortingRepository<BookMaster, Long>, CrudRepository<BookMaster, Long> {

  @Query(value = """
    select b from BookMaster b
      where
        (b.id = :id or :id is null)
        and
        (b.name = :name or :name is null)
        and
        (b.publicationDate >= :publicationDateBegin or :publicationDateBegin is null)
        and
        (b.publicationDate <= :publicationDateEnd or :publicationDateEnd is null)
  """
  )
  Page<BookMaster> findByComplexConditions(
      Long id,
      String name,
      LocalDate publicationDateBegin,
      LocalDate publicationDateEnd,
      Pageable pageable);
}

しかし、これではクエリパラメーター publicationDateBegin=2024-12-19 を渡したときに以下のインターナルサーバーエラーとなります。何が原因でしょうか?

2024-12-19T12:49:28.873Z DEBUG 13187 --- [nio-8080-exec-8] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC ex
ception executing SQL [select bm1_0.id,bm1_0.name,bm1_0.ndc_category_id,bm1_0.publication_date from book_master bm1_0 where (bm1_0.id=? or ? is null) and (bm1_0.name=? or ? is null) and (bm1_0.publicati
on_date=? or ? is null) and (bm1_0.publication_date=? or ? is null) fetch first ? rows only] [ERROR: could not determine data type of parameter $6] [n/a]; SQL [n/a]
2024-12-19T12:49:28.874Z ERROR 13187 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request p
rocessing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL [select bm1_0.id,bm1_0.name,bm1_0.ndc_category_id,bm1_0.publication_date from book_master 
bm1_0 where (bm1_0.id=? or ? is null) and (bm1_0.name=? or ? is null) and (bm1_0.publication_date=? or ? is null) and (bm1_0.publication_date=? or ? is null) fetch first ? rows only] [ERROR: could not d
etermine data type of parameter $6] [n/a]; SQL [n/a]] with root cause

org.postgresql.util.PSQLException: ERROR: could not determine data type of parameter $6
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2733) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2420) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:517) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:434) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:194) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:137) ~[postgresql-42.7.4.jar:42.7.4]
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52) ~[HikariCP-5.1.0.jar:na]
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java) ~[HikariCP-5.1.0.jar:na]
        at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:250) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:171) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.<init>(JdbcValuesResultSetImpl.java:74) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.resolveJdbcValuesSource(JdbcSelectExecutorStandardImpl.java:355) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:137) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:102) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:91) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:165) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$1(ConcreteSqmSelectQueryPlan.java:152) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:442) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:362) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:380) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:143) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.Query.getResultList(Query.java:120) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.springframework.data.jpa.repository.query.JpaQueryExecution$PagedExecution.doExecute(JpaQueryExecution.java:205) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:93) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:152) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:140) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:170) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.0.jar:6.2.0]
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.0.jar:6.2.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138) ~[spring-tx-6.2.0.jar:6.2.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:136) ~[spring-data-jp
a-3.4.0.jar:3.4.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.2.0.jar:6.2.0]
        at jdk.proxy2/jdk.proxy2.$Proxy150.findByComplexConditions(Unknown Source) ~[na:na]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281) ~[spring-core-6.2.0.jar:6.2.0]
        at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invoke(ReflectionRepositoryInvoker.java:220) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invokeQueryMethod(ReflectionRepositoryInvoker.java:155) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeQueryMethod(UnwrappingRepositoryInvokerFactory.java:97) ~[spring-data-rest-core
-4.4.0.jar:4.4.0]
        at org.springframework.data.rest.webmvc.RepositorySearchController.executeQueryMethod(RepositorySearchController.java:339) ~[spring-data-rest-webmvc-4.4.0.jar:4.4.0]
        at org.springframework.data.rest.webmvc.RepositorySearchController.executeSearch(RepositorySearchController.java:170) ~[spring-data-rest-webmvc-4.4.0.jar:4.4.0]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.33.jar:6.0]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:

A: キャストが足りなかったらしい

null と突き合わせるときは明示的に型指定が必要っぽい (date(:publicationDateBegin) is null とか date(:publicationDateEnd) is null の部分)

package dev.mikoto2000.study.springboot.web.practice20241215.repository;

import java.time.LocalDate;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import dev.mikoto2000.study.springboot.web.practice20241215.entity.BookMaster;
import dev.mikoto2000.study.springboot.web.practice20241215.projection.DefaultBookMasterProjection;

/**
 * BookMasterRepository
 */
@RepositoryRestResource(excerptProjection = DefaultBookMasterProjection.class)
public interface BookMasterRepository extends PagingAndSortingRepository<BookMaster, Long>, CrudRepository<BookMaster, Long> {

  @Query(value = """
    select b from BookMaster b
      where
        (b.id = :id or :id is null)
        and
        (b.name = :name or :name is null)
        and
        (b.publicationDate >= :publicationDateBegin or date(:publicationDateBegin) is null)
        and
        (b.publicationDate <= :publicationDateEnd or date(:publicationDateEnd) is null)
  """
  )
  Page<BookMaster> findByComplexConditions(
      Long id,
      String name,
      LocalDate publicationDateBegin,
      LocalDate publicationDateEnd,
      Pageable pageable);
}

2024年12月16日月曜日

Vim 本体に evalfunc (Vim script 関数) を追加する

この記事は Vim 駅伝 の 2024/12/16 の記事です。 前回の記事は yuys13 さんによる、 2024/12/13 の「ソフトウェア技術者とコミュニティ活動~vim-jp radioに出演しました~」という記事でした。

次回は 2024/12/18 に投稿される予定です。

この記事はなに?

少し前に Vim script 関数を追加する Pull Request を出したので、その流れを忘れないように記録するための記事です。

VimConf 2024 の Kato さんのセッションとかぶってしまっていますが、実装の一例として見ていただくのがいいかと思います。 セッションの方がより深掘りされているので、視聴がおすすめです。

今回は、足し算をする関数を追加してみる。

前提

  • Vim のビルド環境が整った状態からスタート

手順概要

  1. 関数の実体を実装
  2. 関数のエントリーポイントを追加
  3. ドキュメントを追加
    1. 関数の説明を追加
    2. タグを追加
    3. 関数リストに追加
  4. 関数のテストを実装

実装

関数の実体を実装

evalfunc.c:

一番下に、実装する。

diff --git a/src/evalfunc.c b/src/evalfunc.c
index b2905da2a..99527029e 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -12125,3 +12125,9 @@ f_xor(typval_T *argvars, typval_T *rettv)
 }
-
 #endif // FEAT_EVAL
+
+    int
+add_num(int x, int y)
+{
+    return x + y;
+}

関数のエントリーポイントを追加

引数パターンの確認

今回追加するのは、ふたつの引数がそれぞれ「数値」、「数値」の引数を持つ関数。

Lists of functions that check the argument types of a builtin function. と コメントされている行から下に、引数のパターンごとに変数がつくられている。

今回追加したいパターンの引数の組み合わせは arg2_number[] となる。

エントリーポイント関数の追加

evalfunc.c:

今回は、さきほど実装した「関数の実体」の直後にエントリーポイントを実装し、プロトタイプ宣言も追加する。

diff --git a/src/evalfunc.c b/src/evalfunc.c
index 99527029e..8fcdaaf60 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -194,6 +194,7 @@ static void f_wildmenumode(typval_T *argvars, typval_T *rettv);
 static void f_windowsversion(typval_T *argvars, typval_T *rettv);
 static void f_wordcount(typval_T *argvars, typval_T *rettv);
 static void f_xor(typval_T *argvars, typval_T *rettv);
+static void f_add_num(typval_T *argvars, typval_T *rettv);
 
 
 /*
@@ -12131,3 +12131,26 @@ add_num(int x, int y)
 {
     return x + y;
 }
+
+/*
+ * "add_num(number, number)" function
+ */
+    static void
+f_add_num(typval_T *argvars, typval_T *rettv)
+{
+    // argvars[] の第一引数と第二引数の型チェック
+    if (in_vim9script()
+           && (check_for_number_arg(argvars, 0) == FAIL
+               || check_for_number_arg(argvars, 1) == FAIL))
+       return;
+
+    // argvars[] から第一引数と第二引数を取得
+    int x = tv_get_number_chk(&argvars[0], NULL);
+    int y = tv_get_number_chk(&argvars[1], NULL);
+
+    // 戻り値に計算結果を格納
+    rettv->vval.v_number = add_num(x, y);
+
+    return;
+}
+

エントリーポイント定義の追加

evalfunc.c:

1758 行目くらいからあるエントリーポイントの定義に、今回実装したエントリーポイントの定義を追加する。

diff --git a/src/evalfunc.c b/src/evalfunc.c
index 99527029e..d51b6ac1f 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -1758,6 +1758,8 @@ static funcentry_T global_functions[] =
                        ret_float,          f_acos},
     {"add",            2, 2, FEARG_1,      arg2_listblobmod_item,
                        ret_first_arg,      f_add},
+    {"add_num",        2, 2, 0,            arg2_number,
+                       ret_number,         f_add_num},
     {"and",            2, 2, FEARG_1,      arg2_number,
                        ret_number,         f_and},
     {"append",         2, 2, FEARG_2,      arg2_setline,

各定義は以下の通り。

  1. 関数名
  2. 最小引数数
  3. 最大引数数
  4. メソッドとして利用できる引数の位置
  5. 引数の組み合わせ
  6. 戻り値の型
  7. 実際に呼び出すエントリーポイント

ドキュメントを追加

関数の説明を追加

runtime/doc/builtin.txt に関数のドキュメントを辞書順になるように追加する。

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index d0f0c7b03..7370091a0 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -26,6 +26,7 @@ USAGE                         RESULT  DESCRIPTION     ~
 abs({expr})                    Float or Number  absolute value of {expr}
 acos({expr})                   Float   arc cosine of {expr}
 add({object}, {item})          List/Blob   append {item} to {object}
+add_num({number}, {number})    Number  add two number
 and({expr}, {expr})            Number  bitwise AND
 append({lnum}, {text})         Number  append {text} below line {lnum}
 appendbufline({buf}, {lnum}, {text})
@@ -833,6 +834,12 @@ add({object}, {expr})                                      *add()*
                |Blob|
 
 
+add_num({number}, {number})                            *add_num()*
+               Add two {number}.
+
+               Return type: |Number|
+
+
 and({expr}, {expr})                                    *and()*
                Bitwise AND on the two arguments.  The arguments are converted
                to a number.  A List, Dict or Float argument causes an error.

タグを追加

runtime/doc/tags に関数のドキュメントのタグを辞書順になるように追加する。

diff --git a/runtime/doc/tags b/runtime/doc/tags
index f33988020..f92a144df 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -5995,6 +5995,7 @@ ada-extra-plugins ft_ada.txt      /*ada-extra-plugins*
 ada-reference  ft_ada.txt      /*ada-reference*
 ada.vim        ft_ada.txt      /*ada.vim*
 add()  builtin.txt     /*add()*
+add_num()      builtin.txt     /*add_num()*
 add-filetype-plugin    usr_05.txt      /*add-filetype-plugin*
 add-global-plugin      usr_05.txt      /*add-global-plugin*
 add-local-help usr_05.txt      /*add-local-help*

関数リストに追加

runtime/doc/usr_41.tx に関数定義を辞書順になるように追加。

diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 36907d249..29e6d2ffc 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -808,6 +808,7 @@ List manipulation:                                  *list-functions*
        empty()                 check if List is empty
        insert()                insert an item somewhere in a List
        add()                   append an item to a List
+       add_num()               add two number
        extend()                append a List to a List
        extendnew()             make a new List and append items
        remove()                remove one or more items from a List

関数のテストを実装

今回は、ただの関数なので src/testdir/test_functions.vim にテスト関数を追加する。

diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 8b2518f2b..9167b2952 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -4206,4 +4206,9 @@ func Test_getcellpixels_gui()
   endif
 endfunc
 
+func Test_add_num()
+  let result = add_num(1, 2)
+  call assert_equal(3, result)
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab

make test_functions で、このファイル内のテストのみを実行できる。

参考資料

2024年12月10日火曜日

Vim の組み込み関数の結果をバッファに出力する

この記事は Vim advent calendar 2024(Adventar) の10日目の記事です。

この記事はなに?

getcellpixels() の追加を行った際に、 テストでバッファに関数の結果を出力する必要があったので、やり方を調べた。

結論

:redi @"
:echo getcellpixels()
:redi END
""p

ここでは、 :redi @" で、コマンドの出力を " レジスタへリダイレクト開始の設定をしている。

その後、 :echo getcellpixels()getcellpixels() 関数の結果を出力しているが、 先のリダイレクト設定により、その結果が " レジスタに格納される。

:redi END でリダイレクト設定を解除し、 ""p" レジスタの内容をバッファにペーストする。

こうすることで、空のバッファにペーストした際には、バッファの 3 行目にコマンドの結果が出力される。

3 行目である理由は謎。

以上。

参考資料

2024年12月6日金曜日

keyinput-delayer.vim で Vim での編集力を鍛える

この記事は Vim advent calendar 2024(Adventar) の4日目の記事です。

この記事はなに?

mikoto2000/keyinput-delayer.vim: 別名「Vimmer 養成ギプス」。 を作ったので、その宣伝です。

人はついつい「知っている方法・慣れた方法」で物事を進めてしまう。

それは Vim における編集でも同じことで、ついつい h, j, k, l での移動をしてしまいがちです。

それはなぜかというと、「キー入力のコストがとても低いから」であると私は考えました。

キー入力のコストがもっと高ければ、丁寧丁寧丁寧に操作を吟味してキー入力をすることでしょう。

例えば、帯域の狭い回線の SSH で、エコーバックがなかなか帰ってこない時、いつもより丁寧に操作をしたことはありませんか?

そんな環境を Vim 上で再現するのが mikoto2000/keyinput-delayer.vim, 別名「Vimmer 養成ギプス」です。

使い方

let g:keyinput_delayer_delay_time = "1000m" のようにディレイ時間を設定し、 keyinput_delayer#ToggleKeyInputDelay() を呼び出すことで、ディレイのあり・なしを切り替えます。

例えばディレイを 1 秒にしたとき、 10 行下に移動するために必要な時間は以下のようになります。

  • j x 10 : 10 秒
  • 10j : 3 秒

これを見て分かる通り、キーインプットのコストが非常に高くなるため、 「タイプ数を減らして編集を実現する」という意識が高くなります。

皆さんも keyinput-delayer.vim を導入して Vim のキーストロークゴルフ思考を身に着けていきましょう!

類似プラグイン

同じ目的のプラグインが存在することを vim-jp で教えていただきました。

こちらは、連続の 1 マス移動でカーソルが全く動かなくなるので、 よりストイックに鍛えたい方に向いているのではないでしょうか?

以上、 keyinput-delayer.vim の宣伝でした。

2024年12月3日火曜日

Nix で Vim をスタティックリンクビルドする。ついでにクロスコンパイルもする

この記事はなに?

mikoto2000/devcontainer.vim の ARM 対応で、 ARM 版のスタティックリンクな Vim が必要になった。

vim-jp にて「Nix ならコマンド一発でできるよ」と教えてもらったのでやってみた。

前提

  • Docker: Docker version 27.3.1, build ce12230
  • 使用イメージ: nixos/nix

Nix のリポジトリアップデート

nix-channel --update

スタティックリンクビルド

nix-build '<nixpkgs>' --cores 20 -A pkgsStatic.vim
  • --cores: make でいうところの -j オプション

aarch64 のクロスコンパイル&スタティックリンクビルド

nix-build '<nixpkgs>' --cores 20 -A pkgsCross.aarch64-multiplatform.pkgsStatic.vim

ビルドされた Vim の場所

find で探しましょう。

bash-5.2# find /nix -name vim
/nix/store/jx5d1dp77cwkarvalh1l8zvfp2ziqzlw-vim-static-aarch64-unknown-linux-musl-9.1.0787/share/vim
/nix/store/jx5d1dp77cwkarvalh1l8zvfp2ziqzlw-vim-static-aarch64-unknown-linux-musl-9.1.0787/bin/vim
/nix/store/7wkl9dji0dq6v7nmdjp1d72wd5pydqpb-vim-static-x86_64-unknown-linux-musl-9.1.0787/share/vim
/nix/store/7wkl9dji0dq6v7nmdjp1d72wd5pydqpb-vim-static-x86_64-unknown-linux-musl-9.1.0787/bin/vim
/nix/store/gnzqq5zg8r55apxa5avlb5yp0ix8qdwk-nixpkgs/nixpkgs/pkgs/applications/editors/vim
/nix/store/gnzqq5zg8r55apxa5avlb5yp0ix8qdwk-nixpkgs/nixpkgs/pkgs/test/vim
/nix/store/yzqnkb2mfzphmhv3248pw0pqh6f1mn6y-7sprarsdfz9qcd7859phvr9nvhi14mri-source/pkgs/applications/editors/vim
/nix/store/yzqnkb2mfzphmhv3248pw0pqh6f1mn6y-7sprarsdfz9qcd7859phvr9nvhi14mri-source/pkgs/test/vim

bin 下のが目的のバイナリですね。

share の方にはランタイムが入っています。

パッケージング

Vim は、ランタイムと一緒に配布しないと上手く動かないので、パッケージングを行う。 さらに、ランタイムの場所はビルド時に決め打ちされるため、 VIM 環境変数を上書きして Vim を起動するシェルスクリプトを用意し、それ経由で実行してもらうようにする。 (今回の場合は AppRun というシェルスクリプトを用意)

# aarch64 のパッケージング
cp -a /nix/store/jx5d1dp77cwkarvalh1l8zvfp2ziqzlw-vim-static-aarch64-unknown-linux-musl-9.1.0787 vim-9.1.0787-aarch64
cd vim-9.1.0787-aarch64
cat << "EOF" > ./AppRun
#!/bin/sh
CURRENT_DIR="$(dirname "$(realpath "$0")")"
export VIM="${CURRENT_DIR}/share/vim/"
exec "${CURRENT_DIR}/bin/vim" "$@"
EOF
chmod a+x ./AppRun
cd ..
tar zcfv ./vim-9.1.0787-aarch64.tar.gz vim-9.1.0787-aarch64

# aarch64 のパッケージング
cp -a /nix/store/7wkl9dji0dq6v7nmdjp1d72wd5pydqpb-vim-static-x86_64-unknown-linux-musl-9.1.0787 vim-9.1.0787-x86_64
cd vim-9.1.0787-x86_64
cat << "EOF" > ./AppRun
#!/bin/sh
CURRENT_DIR="$(dirname "$(realpath "$0")")"
export VIM="${CURRENT_DIR}/share/vim/"
exec "${CURRENT_DIR}/bin/vim" "$@"
EOF
chmod a+x ./AppRun
cd ..
tar zcfv ./vim-9.1.0787-x86_64.tar.gz ./vim-9.1.0787-x86_64

これで、「ダウンロードした tar.gz を展開して、その中の AppRun を実行すれば vim が起動する」という感じになります。

devcontainer.vim で必要なので、このスタティックリンクな Vim を mikoto2000/vim-static: Distributing a static link binary for vim/vim. で配布予定です。

以上。

参考資料