From 08958af670562d7185b4333a79a45b33cf39c032 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Thu, 8 May 2025 16:39:21 -0400 Subject: [PATCH] Add flexible support for multiple database variations in JdbcChatMemory Add SQL Server dialect support with new SqlServerChatMemoryDialect class Create schema-sqlserver.sql script for SQL Server table creation Refactor database schema initialization with new JdbcChatMemorySchemaInitializer Standardize table names across databases to SPRING_AI_CHAT_MEMORY Implement ChatMemoryDialect abstraction for database-specific operations Add dialect implementations for PostgreSQL, MySQL/MariaDB, HSQLDB Update configuration properties with more flexible schema initialization options Enhance documentation with detailed information about dialect support and configuration Add integration tests for HSQLDB and SQL Server Signed-off-by: Mark Pollack <247466+markpollack@users.noreply.github.com> --- .../pom.xml | 17 ++ ...ryDataSourceScriptDatabaseInitializer.java | 56 ------ .../JdbcChatMemoryProperties.java | 44 ---- ...hatMemoryRepositoryAutoConfiguration.java} | 23 +-- .../JdbcChatMemoryRepositoryProperties.java | 78 +++++++ ...ChatMemoryRepositorySchemaInitializer.java | 92 +++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 2 +- ...bcChatMemoryHsqldbAutoConfigurationIT.java | 190 ++++++++++++++++++ ...atMemoryPostgresqlAutoConfigurationIT.java | 66 +++--- ...cChatMemoryRepositoryPropertiesTests.java} | 18 +- ...torySchemaInitializerPostgresqlTests.java} | 10 +- ...hatMemorySqlServerAutoConfigurationIT.java | 110 ++++++++++ .../src/test/resources/schema.sql | 12 ++ .../memory/jdbc/HsqldbChatMemoryDialect.java | 44 ++++ .../memory/jdbc/JdbcChatMemoryDialect.java | 75 +++++++ .../memory/jdbc/JdbcChatMemoryRepository.java | 41 ++-- .../memory/jdbc/MysqlChatMemoryDialect.java | 47 +++++ .../jdbc/PostgresChatMemoryDialect.java | 47 +++++ .../jdbc/SqlServerChatMemoryDialect.java | 47 +++++ .../JdbcChatMemoryRepositoryRuntimeHints.java | 4 +- .../ai/chat/memory/jdbc/schema-hsqldb.sql | 10 + .../ai/chat/memory/jdbc/schema-mariadb.sql | 8 +- .../ai/chat/memory/jdbc/schema-postgresql.sql | 6 +- .../ai/chat/memory/jdbc/schema-sqlserver.sql | 9 + .../modules/ROOT/pages/api/chat-memory.adoc | 52 ++++- 25 files changed, 913 insertions(+), 195 deletions(-) delete mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializer.java delete mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryProperties.java rename auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/{jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java => repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryAutoConfiguration.java} (72%) create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryProperties.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializer.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryHsqldbAutoConfigurationIT.java rename auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/{ => repository}/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java (64%) rename auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/{jdbc/autoconfigure/JdbcChatMemoryPropertiesTests.java => repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryPropertiesTests.java} (58%) rename auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/{jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests.java => repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializerPostgresqlTests.java} (84%) create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemorySqlServerAutoConfigurationIT.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/resources/schema.sql create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/HsqldbChatMemoryDialect.java create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryDialect.java create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/MysqlChatMemoryDialect.java create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/PostgresChatMemoryDialect.java create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/SqlServerChatMemoryDialect.java create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-hsqldb.sql create mode 100644 memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-sqlserver.sql diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml index df76c788c..075b40ae5 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml @@ -78,6 +78,23 @@ postgresql test + + + com.microsoft.sqlserver + mssql-jdbc + test + + + org.testcontainers + mssqlserver + test + + + + org.hsqldb + hsqldb + runtime + diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializer.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializer.java deleted file mode 100644 index 546b272d3..000000000 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializer.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.model.chat.memory.jdbc.autoconfigure; - -import java.util.List; - -import javax.sql.DataSource; - -import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; -import org.springframework.boot.jdbc.init.PlatformPlaceholderDatabaseDriverResolver; -import org.springframework.boot.sql.init.DatabaseInitializationMode; -import org.springframework.boot.sql.init.DatabaseInitializationSettings; - -/** - * Performs database initialization for the JDBC Chat Memory Repository. - * - * @since 1.0.0 - */ -class JdbcChatMemoryDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer { - - private static final String SCHEMA_LOCATION = "classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql"; - - JdbcChatMemoryDataSourceScriptDatabaseInitializer(DataSource dataSource) { - super(dataSource, getSettings(dataSource)); - } - - static DatabaseInitializationSettings getSettings(DataSource dataSource) { - var settings = new DatabaseInitializationSettings(); - settings.setSchemaLocations(resolveSchemaLocations(dataSource)); - settings.setMode(DatabaseInitializationMode.ALWAYS); - settings.setContinueOnError(true); - - return settings; - } - - static List resolveSchemaLocations(DataSource dataSource) { - var platformResolver = new PlatformPlaceholderDatabaseDriverResolver(); - - return platformResolver.resolveAll(dataSource, SCHEMA_LOCATION); - } - -} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryProperties.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryProperties.java deleted file mode 100644 index 6ff1a2a18..000000000 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryProperties.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.model.chat.memory.jdbc.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * @author Jonathan Leijendekker - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(JdbcChatMemoryProperties.CONFIG_PREFIX) -public class JdbcChatMemoryProperties { - - public static final String CONFIG_PREFIX = "spring.ai.chat.memory.repository.jdbc"; - - /** - * Whether to initialize the schema on startup. - */ - private boolean initializeSchema = true; - - public boolean isInitializeSchema() { - return this.initializeSchema; - } - - public void setInitializeSchema(boolean initializeSchema) { - this.initializeSchema = initializeSchema; - } - -} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryAutoConfiguration.java similarity index 72% rename from auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryAutoConfiguration.java index a6942b07d..2d0a89403 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryAutoConfiguration.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package org.springframework.ai.model.chat.memory.jdbc.autoconfigure; +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryDialect; import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryRepository; import org.springframework.ai.model.chat.memory.autoconfigure.ChatMemoryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -39,24 +39,23 @@ import org.springframework.jdbc.core.JdbcTemplate; */ @AutoConfiguration(after = JdbcTemplateAutoConfiguration.class, before = ChatMemoryAutoConfiguration.class) @ConditionalOnClass({ JdbcChatMemoryRepository.class, DataSource.class, JdbcTemplate.class }) -@EnableConfigurationProperties(JdbcChatMemoryProperties.class) -public class JdbcChatMemoryAutoConfiguration { +@EnableConfigurationProperties(JdbcChatMemoryRepositoryProperties.class) +public class JdbcChatMemoryRepositoryAutoConfiguration { - private static final Logger logger = LoggerFactory.getLogger(JdbcChatMemoryAutoConfiguration.class); + private static final Logger logger = LoggerFactory.getLogger(JdbcChatMemoryRepositoryAutoConfiguration.class); @Bean @ConditionalOnMissingBean - JdbcChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate) { - return JdbcChatMemoryRepository.builder().jdbcTemplate(jdbcTemplate).build(); + JdbcChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate, DataSource dataSource) { + JdbcChatMemoryDialect dialect = JdbcChatMemoryDialect.from(dataSource); + return JdbcChatMemoryRepository.builder().jdbcTemplate(jdbcTemplate).dialect(dialect).build(); } @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = JdbcChatMemoryProperties.CONFIG_PREFIX, name = "initialize-schema", - havingValue = "true", matchIfMissing = true) - JdbcChatMemoryDataSourceScriptDatabaseInitializer jdbcChatMemoryScriptDatabaseInitializer(DataSource dataSource) { - logger.debug("Initializing schema for JdbcChatMemoryRepository"); - return new JdbcChatMemoryDataSourceScriptDatabaseInitializer(dataSource); + JdbcChatMemoryRepositorySchemaInitializer jdbcChatMemoryScriptDatabaseInitializer(DataSource dataSource, + JdbcChatMemoryRepositoryProperties properties) { + return new JdbcChatMemoryRepositorySchemaInitializer(dataSource, properties); } } diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryProperties.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryProperties.java new file mode 100644 index 000000000..47e64e5a9 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryProperties.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Jonathan Leijendekker + * @author Thomas Vitale + * @since 1.0.0 + */ +@ConfigurationProperties(JdbcChatMemoryRepositoryProperties.CONFIG_PREFIX) +public class JdbcChatMemoryRepositoryProperties { + + public static final String CONFIG_PREFIX = "spring.ai.chat.memory.repository.jdbc"; + + /** + * Whether to initialize the schema on startup. Values: embedded, always, never. + * Default is embedded. + */ + private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED; + + /** + * Locations of schema (DDL) scripts. Supports comma-separated list. Default is + * classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql + */ + private String schema = "classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql"; + + public DatabaseInitializationMode getInitializeSchema() { + return this.initializeSchema; + } + + public void setInitializeSchema(DatabaseInitializationMode initializeSchema) { + this.initializeSchema = initializeSchema; + } + + public String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public enum DatabaseInitializationMode { + + /** + * Always initialize the database. + */ + ALWAYS, + + /** + * Only initialize an embedded database. + */ + EMBEDDED, + + /** + * Never initialize the database. + */ + NEVER + + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializer.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializer.java new file mode 100644 index 000000000..e202e4794 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializer.java @@ -0,0 +1,92 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; + +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.jdbc.init.PlatformPlaceholderDatabaseDriverResolver; +import org.springframework.boot.sql.init.DatabaseInitializationMode; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.util.StringUtils; + +/** + * Performs database initialization for the JDBC Chat Memory Repository. + * + * @author Mark Pollack + * @since 1.0.0 + */ +class JdbcChatMemoryRepositorySchemaInitializer extends DataSourceScriptDatabaseInitializer { + + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql"; + + JdbcChatMemoryRepositorySchemaInitializer(DataSource dataSource, JdbcChatMemoryRepositoryProperties properties) { + super(dataSource, getSettings(dataSource, properties)); + } + + static DatabaseInitializationSettings getSettings(DataSource dataSource, + JdbcChatMemoryRepositoryProperties properties) { + var settings = new DatabaseInitializationSettings(); + + // Determine schema locations + String schemaProp = properties.getSchema(); + List schemaLocations; + PlatformPlaceholderDatabaseDriverResolver resolver = new PlatformPlaceholderDatabaseDriverResolver(); + try { + String url = dataSource.getConnection().getMetaData().getURL().toLowerCase(); + if (url.contains("hsqldb")) { + schemaLocations = List.of("classpath:org/springframework/ai/chat/memory/jdbc/schema-hsqldb.sql"); + } + else if (StringUtils.hasText(schemaProp)) { + schemaLocations = resolver.resolveAll(dataSource, schemaProp); + } + else { + schemaLocations = resolver.resolveAll(dataSource, DEFAULT_SCHEMA_LOCATION); + } + } + catch (Exception e) { + // fallback to default + if (StringUtils.hasText(schemaProp)) { + schemaLocations = resolver.resolveAll(dataSource, schemaProp); + } + else { + schemaLocations = resolver.resolveAll(dataSource, DEFAULT_SCHEMA_LOCATION); + } + } + settings.setSchemaLocations(schemaLocations); + + // Determine initialization mode + JdbcChatMemoryRepositoryProperties.DatabaseInitializationMode init = properties.getInitializeSchema(); + DatabaseInitializationMode mode; + if (JdbcChatMemoryRepositoryProperties.DatabaseInitializationMode.ALWAYS.equals(init)) { + mode = DatabaseInitializationMode.ALWAYS; + } + else if (JdbcChatMemoryRepositoryProperties.DatabaseInitializationMode.NEVER.equals(init)) { + mode = DatabaseInitializationMode.NEVER; + } + else { + // embedded or default + mode = DatabaseInitializationMode.EMBEDDED; + } + settings.setMode(mode); + settings.setContinueOnError(true); + return settings; + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6820c4237..b9a864a07 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.chat.memory.jdbc.autoconfigure.JdbcChatMemoryAutoConfiguration +org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure.JdbcChatMemoryRepositoryAutoConfiguration diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryHsqldbAutoConfigurationIT.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryHsqldbAutoConfigurationIT.java new file mode 100644 index 000000000..cadfeb96b --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryHsqldbAutoConfigurationIT.java @@ -0,0 +1,190 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = JdbcChatMemoryHsqldbAutoConfigurationIT.TestConfig.class, + properties = { "spring.datasource.url=jdbc:hsqldb:mem:chat_memory_auto_configuration_test;DB_CLOSE_DELAY=-1", + "spring.datasource.username=sa", "spring.datasource.password=", + "spring.datasource.driver-class-name=org.hsqldb.jdbcDriver", + "spring.ai.chat.memory.repository.jdbc.initialize-schema=always", "spring.sql.init.mode=always", + "spring.jpa.hibernate.ddl-auto=none", "spring.jpa.defer-datasource-initialization=true", + "spring.sql.init.continue-on-error=true", "spring.sql.init.schema-locations=classpath:schema.sql", + "logging.level.org.springframework.jdbc=DEBUG", + "logging.level.org.springframework.boot.sql.init=DEBUG" }) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +@ImportAutoConfiguration({ org.springframework.ai.model.chat.memory.autoconfigure.ChatMemoryAutoConfiguration.class, + JdbcChatMemoryRepositoryAutoConfiguration.class, + org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration.class, + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class, + SqlInitializationAutoConfiguration.class }) +public class JdbcChatMemoryHsqldbAutoConfigurationIT { + + @Autowired + private ApplicationContext context; + + @Autowired + private JdbcTemplate jdbcTemplate; + + /** + * can't get the automatic loading of the schema with boot to work. + */ + @Before + public void setUp() { + // Explicitly initialize the schema + try { + System.out.println("Explicitly initializing schema..."); + + // Debug: Print current schemas and tables + try { + List schemas = jdbcTemplate.queryForList("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA", + String.class); + System.out.println("Available schemas: " + schemas); + + List tables = jdbcTemplate + .queryForList("SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES", String.class); + System.out.println("Available tables: " + tables); + } + catch (Exception e) { + System.out.println("Error listing schemas/tables: " + e.getMessage()); + } + + // Try a more direct approach with explicit SQL statements + try { + // Drop the table first if it exists to avoid any conflicts + jdbcTemplate.execute("DROP TABLE SPRING_AI_CHAT_MEMORY IF EXISTS"); + System.out.println("Dropped existing table if it existed"); + + // Create the table with a simplified schema + jdbcTemplate.execute("CREATE TABLE SPRING_AI_CHAT_MEMORY (" + "conversation_id VARCHAR(36) NOT NULL, " + + "content LONGVARCHAR NOT NULL, " + "type VARCHAR(10) NOT NULL, " + + "timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL)"); + System.out.println("Created table with simplified schema"); + + // Create index + jdbcTemplate.execute( + "CREATE INDEX SPRING_AI_CHAT_MEMORY_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, timestamp DESC)"); + System.out.println("Created index"); + + // Verify table was created + boolean tableExists = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'SPRING_AI_CHAT_MEMORY'", + Integer.class) > 0; + System.out.println("Table SPRING_AI_CHAT_MEMORY exists after creation: " + tableExists); + } + catch (Exception e) { + System.out.println("Error during direct table creation: " + e.getMessage()); + e.printStackTrace(); + } + + System.out.println("Schema initialization completed"); + } + catch (Exception e) { + System.out.println("Error during explicit schema initialization: " + e.getMessage()); + e.printStackTrace(); + } + } + + @Test + public void useAutoConfiguredChatMemoryWithJdbc() { + // Check that the custom schema initializer is present + assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue(); + + // Debug: List all schema-hsqldb.sql resources on the classpath + try { + java.util.Enumeration resources = Thread.currentThread() + .getContextClassLoader() + .getResources("org/springframework/ai/chat/memory/jdbc/schema-hsqldb.sql"); + System.out.println("--- schema-hsqldb.sql resources found on classpath ---"); + while (resources.hasMoreElements()) { + System.out.println(resources.nextElement()); + } + System.out.println("------------------------------------------------------"); + } + catch (Exception e) { + e.printStackTrace(); + } + + // Verify the table exists by executing a direct query + try { + boolean tableExists = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'SPRING_AI_CHAT_MEMORY'", + Integer.class) > 0; + System.out.println("Table SPRING_AI_CHAT_MEMORY exists: " + tableExists); + assertThat(tableExists).isTrue(); + } + catch (Exception e) { + System.out.println("Error checking table: " + e.getMessage()); + e.printStackTrace(); + fail("Failed to check if table exists: " + e.getMessage()); + } + + // Now test the ChatMemory functionality + assertThat(context.getBean(org.springframework.ai.chat.memory.ChatMemory.class)).isNotNull(); + assertThat(context.getBean(org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryRepository.class)).isNotNull(); + + var chatMemory = context.getBean(org.springframework.ai.chat.memory.ChatMemory.class); + var conversationId = java.util.UUID.randomUUID().toString(); + var userMessage = new UserMessage("Message from the user"); + + chatMemory.add(conversationId, userMessage); + assertThat(chatMemory.get(conversationId)).hasSize(1); + assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage)); + + var assistantMessage = new AssistantMessage("Message from the assistant"); + chatMemory.add(conversationId, List.of(assistantMessage)); + assertThat(chatMemory.get(conversationId)).hasSize(2); + assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage, assistantMessage)); + + chatMemory.clear(conversationId); + assertThat(chatMemory.get(conversationId)).isEmpty(); + + var multipleMessages = List.of(new UserMessage("Message from the user 1"), + new AssistantMessage("Message from the assistant 1")); + chatMemory.add(conversationId, multipleMessages); + assertThat(chatMemory.get(conversationId)).hasSize(multipleMessages.size()); + assertThat(chatMemory.get(conversationId)).isEqualTo(multipleMessages); + } + + @SpringBootConfiguration + static class TestConfig { + + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java similarity index 64% rename from auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java index 5961f25ee..1631eb707 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.chat.memory.jdbc.autoconfigure; +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; import java.util.List; import java.util.UUID; @@ -42,27 +42,29 @@ import static org.assertj.core.api.Assertions.assertThat; class JdbcChatMemoryPostgresqlAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JdbcChatMemoryAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(JdbcChatMemoryRepositoryAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class)) .withPropertyValues("spring.datasource.url=jdbc:tc:postgresql:17:///"); @Test void jdbcChatMemoryScriptDatabaseInitializer_shouldBeLoaded() { - this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=true") - .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); - this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=true") + this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=always") .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); } @Test - void jdbcChatMemoryScriptDatabaseInitializer_shouldNotBeLoaded() { - this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=false") - .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isFalse()); + void jdbcChatMemoryScriptDatabaseInitializer_shouldNotRunSchemaInit() { + this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=never") + .run(context -> { + assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue(); + // Optionally, check that the schema is not initialized (could check table + // absence if needed) + }); } @Test - void initializeSchemaEnabledWithProperty() { - this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=true") + void initializeSchemaEmbeddedDefault() { + this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded") .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); } @@ -94,38 +96,40 @@ class JdbcChatMemoryPostgresqlAutoConfigurationIT { @Test void useAutoConfiguredChatMemoryWithJdbc() { - this.contextRunner.withConfiguration(AutoConfigurations.of(ChatMemoryAutoConfiguration.class)).run(context -> { - assertThat(context).hasSingleBean(ChatMemory.class); - assertThat(context).hasSingleBean(JdbcChatMemoryRepository.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(ChatMemoryAutoConfiguration.class)) + .withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=always") + .run(context -> { + assertThat(context).hasSingleBean(ChatMemory.class); + assertThat(context).hasSingleBean(JdbcChatMemoryRepository.class); - var chatMemory = context.getBean(ChatMemory.class); - var conversationId = UUID.randomUUID().toString(); - var userMessage = new UserMessage("Message from the user"); + var chatMemory = context.getBean(ChatMemory.class); + var conversationId = UUID.randomUUID().toString(); + var userMessage = new UserMessage("Message from the user"); - chatMemory.add(conversationId, userMessage); + chatMemory.add(conversationId, userMessage); - assertThat(chatMemory.get(conversationId)).hasSize(1); - assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage)); + assertThat(chatMemory.get(conversationId)).hasSize(1); + assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage)); - var assistantMessage = new AssistantMessage("Message from the assistant"); + var assistantMessage = new AssistantMessage("Message from the assistant"); - chatMemory.add(conversationId, List.of(assistantMessage)); + chatMemory.add(conversationId, List.of(assistantMessage)); - assertThat(chatMemory.get(conversationId)).hasSize(2); - assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage, assistantMessage)); + assertThat(chatMemory.get(conversationId)).hasSize(2); + assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage, assistantMessage)); - chatMemory.clear(conversationId); + chatMemory.clear(conversationId); - assertThat(chatMemory.get(conversationId)).isEmpty(); + assertThat(chatMemory.get(conversationId)).isEmpty(); - var multipleMessages = List.of(new UserMessage("Message from the user 1"), - new AssistantMessage("Message from the assistant 1")); + var multipleMessages = List.of(new UserMessage("Message from the user 1"), + new AssistantMessage("Message from the assistant 1")); - chatMemory.add(conversationId, multipleMessages); + chatMemory.add(conversationId, multipleMessages); - assertThat(chatMemory.get(conversationId)).hasSize(multipleMessages.size()); - assertThat(chatMemory.get(conversationId)).isEqualTo(multipleMessages); - }); + assertThat(chatMemory.get(conversationId)).hasSize(multipleMessages.size()); + assertThat(chatMemory.get(conversationId)).isEqualTo(multipleMessages); + }); } } diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPropertiesTests.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryPropertiesTests.java similarity index 58% rename from auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPropertiesTests.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryPropertiesTests.java index 196176149..50f851ef6 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPropertiesTests.java +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.chat.memory.jdbc.autoconfigure; +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; import org.junit.jupiter.api.Test; @@ -23,21 +23,21 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Jonathan Leijendekker */ -class JdbcChatMemoryPropertiesTests { +class JdbcChatMemoryRepositoryPropertiesTests { @Test void defaultValues() { - var props = new JdbcChatMemoryProperties(); - - assertThat(props.isInitializeSchema()).isTrue(); + var props = new JdbcChatMemoryRepositoryProperties(); + assertThat(props.getInitializeSchema()) + .isEqualTo(JdbcChatMemoryRepositoryProperties.DatabaseInitializationMode.EMBEDDED); } @Test void customValues() { - var props = new JdbcChatMemoryProperties(); - props.setInitializeSchema(false); - - assertThat(props.isInitializeSchema()).isFalse(); + var props = new JdbcChatMemoryRepositoryProperties(); + props.setInitializeSchema(JdbcChatMemoryRepositoryProperties.DatabaseInitializationMode.NEVER); + assertThat(props.getInitializeSchema()) + .isEqualTo(JdbcChatMemoryRepositoryProperties.DatabaseInitializationMode.NEVER); } } diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializerPostgresqlTests.java similarity index 84% rename from auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializerPostgresqlTests.java index cd53f2bd8..d4d175fb7 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests.java +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositorySchemaInitializerPostgresqlTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.chat.memory.jdbc.autoconfigure; +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; import javax.sql.DataSource; @@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Jonathan Leijendekker */ @Testcontainers -class JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests { +class JdbcChatMemoryRepositorySchemaInitializerPostgresqlTests { static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres:17"); @@ -47,7 +47,7 @@ class JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests { .withPassword("postgres"); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JdbcChatMemoryAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(JdbcChatMemoryRepositoryAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class)) .withPropertyValues(String.format("spring.datasource.url=%s", postgresContainer.getJdbcUrl()), String.format("spring.datasource.username=%s", postgresContainer.getUsername()), @@ -57,7 +57,9 @@ class JdbcChatMemoryDataSourceScriptDatabaseInitializerPostgresqlTests { void getSettings_shouldHaveSchemaLocations() { this.contextRunner.run(context -> { var dataSource = context.getBean(DataSource.class); - var settings = JdbcChatMemoryDataSourceScriptDatabaseInitializer.getSettings(dataSource); + // Use new signature: requires JdbcChatMemoryRepositoryProperties + var settings = JdbcChatMemoryRepositorySchemaInitializer.getSettings(dataSource, + new JdbcChatMemoryRepositoryProperties()); assertThat(settings.getSchemaLocations()) .containsOnly("classpath:org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql"); diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemorySqlServerAutoConfigurationIT.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemorySqlServerAutoConfigurationIT.java new file mode 100644 index 000000000..72281c19c --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemorySqlServerAutoConfigurationIT.java @@ -0,0 +1,110 @@ +/* + * Integration test for SQL Server using Testcontainers, following the same structure as the PostgreSQL test. + */ +package org.springframework.ai.model.chat.memory.repository.jdbc.autoconfigure; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryRepository; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.model.chat.memory.autoconfigure.ChatMemoryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +class JdbcChatMemorySqlServerAutoConfigurationIT { + + static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName + .parse("mcr.microsoft.com/mssql/server:2022-latest"); + + @Container + @SuppressWarnings("resource") + static MSSQLServerContainer mssqlContainer = new MSSQLServerContainer<>(DEFAULT_IMAGE_NAME).acceptLicense() + .withEnv("MSSQL_DATABASE", "chat_memory_auto_configuration_test") + .withPassword("Strong!NotR34LLyPassword") + .withUrlParam("loginTimeout", "60") // Give more time for the login + .withUrlParam("connectRetryCount", "10") // Retry 10 times + .withUrlParam("connectRetryInterval", "10") + .withStartupTimeout(Duration.ofSeconds(60)); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JdbcChatMemoryRepositoryAutoConfiguration.class, + JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class)) + .withPropertyValues(String.format("spring.datasource.url=%s", mssqlContainer.getJdbcUrl()), + String.format("spring.datasource.username=%s", mssqlContainer.getUsername()), + String.format("spring.datasource.password=%s", mssqlContainer.getPassword())); + + @Test + void jdbcChatMemoryScriptDatabaseInitializer_shouldBeLoaded() { + this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=always") + .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); + } + + @Test + void jdbcChatMemoryScriptDatabaseInitializer_shouldNotRunSchemaInit() { + this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=never") + .run(context -> { + assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue(); + }); + } + + @Test + void initializeSchemaEmbeddedDefault() { + this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded") + .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); + } + + @Test + void useAutoConfiguredChatMemoryWithJdbc() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ChatMemoryAutoConfiguration.class)) + .withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=always") + .run(context -> { + assertThat(context).hasSingleBean(ChatMemory.class); + assertThat(context).hasSingleBean(JdbcChatMemoryRepository.class); + + var chatMemory = context.getBean(ChatMemory.class); + var conversationId = UUID.randomUUID().toString(); + var userMessage = new UserMessage("Message from the user"); + + chatMemory.add(conversationId, userMessage); + + assertThat(chatMemory.get(conversationId)).hasSize(1); + assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage)); + + var assistantMessage = new AssistantMessage("Message from the assistant"); + + chatMemory.add(conversationId, List.of(assistantMessage)); + + assertThat(chatMemory.get(conversationId)).hasSize(2); + assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage, assistantMessage)); + + chatMemory.clear(conversationId); + + assertThat(chatMemory.get(conversationId)).isEmpty(); + + var multipleMessages = List.of(new UserMessage("Message from the user 1"), + new AssistantMessage("Message from the assistant 1")); + + chatMemory.add(conversationId, multipleMessages); + + assertThat(chatMemory.get(conversationId)).hasSize(multipleMessages.size()); + assertThat(chatMemory.get(conversationId)).isEqualTo(multipleMessages); + }); + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/resources/schema.sql b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/resources/schema.sql new file mode 100644 index 000000000..2f34e27d3 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/resources/schema.sql @@ -0,0 +1,12 @@ +-- Test-specific schema initialization for HSQLDB +CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( + conversation_id VARCHAR(36) NOT NULL, + content LONGVARCHAR NOT NULL, + type VARCHAR(10) NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, timestamp DESC); + +-- Add constraint if it doesn't exist +ALTER TABLE SPRING_AI_CHAT_MEMORY ADD CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')); diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/HsqldbChatMemoryDialect.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/HsqldbChatMemoryDialect.java new file mode 100644 index 000000000..39e9770f6 --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/HsqldbChatMemoryDialect.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.chat.memory.jdbc; + +/** + * HSQLDB-specific SQL dialect for chat memory repository. + */ +public class HsqldbChatMemoryDialect implements JdbcChatMemoryDialect { + + @Override + public String getSelectMessagesSql() { + return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY timestamp ASC"; + } + + @Override + public String getInsertMessageSql() { + return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, timestamp) VALUES (?, ?, ?, ?)"; + } + + @Override + public String getDeleteMessagesSql() { + return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?"; + } + + @Override + public String getSelectConversationIdsSql() { + return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY"; + } + +} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryDialect.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryDialect.java new file mode 100644 index 000000000..a186dd595 --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryDialect.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.chat.memory.jdbc; + +import javax.sql.DataSource; + +/** + * Abstraction for database-specific SQL for chat memory repository. + */ +public interface JdbcChatMemoryDialect { + + /** + * Returns the SQL to fetch messages for a conversation, ordered by timestamp, with + * limit. + */ + String getSelectMessagesSql(); + + /** + * Returns the SQL to insert a message. + */ + String getInsertMessageSql(); + + /** + * Returns the SQL to fetch conversation IDs. + */ + String getSelectConversationIdsSql(); + + /** + * Returns the SQL to delete all messages for a conversation. + */ + String getDeleteMessagesSql(); + + /** + * Optionally, dialect can provide more advanced SQL as needed. + */ + + /** + * Detects the dialect from the DataSource or JDBC URL. + */ + static JdbcChatMemoryDialect from(DataSource dataSource) { + // Simple detection (could be improved) + try { + String url = dataSource.getConnection().getMetaData().getURL().toLowerCase(); + if (url.contains("postgresql")) + return new PostgresChatMemoryDialect(); + if (url.contains("mysql")) + return new MysqlChatMemoryDialect(); + if (url.contains("mariadb")) + return new MysqlChatMemoryDialect(); + if (url.contains("sqlserver")) + return new SqlServerChatMemoryDialect(); + if (url.contains("hsqldb")) + return new HsqldbChatMemoryDialect(); + // Add more as needed + } + catch (Exception ignored) { + } + return new PostgresChatMemoryDialect(); // default + } + +} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryRepository.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryRepository.java index cf10413a5..3fbbd3ffb 100644 --- a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryRepository.java +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryRepository.java @@ -44,34 +44,25 @@ import org.springframework.util.Assert; * @author Jonathan Leijendekker * @author Thomas Vitale * @author Linar Abzaltdinov + * @author Mark Pollack * @since 1.0.0 */ public class JdbcChatMemoryRepository implements ChatMemoryRepository { - private static final String QUERY_GET_IDS = """ - SELECT DISTINCT conversation_id FROM ai_chat_memory - """; - - private static final String QUERY_ADD = """ - INSERT INTO ai_chat_memory (conversation_id, content, type, "timestamp") VALUES (?, ?, ?, ?) - """; - - private static final String QUERY_GET = """ - SELECT content, type FROM ai_chat_memory WHERE conversation_id = ? ORDER BY "timestamp" - """; - - private static final String QUERY_CLEAR = "DELETE FROM ai_chat_memory WHERE conversation_id = ?"; - private final JdbcTemplate jdbcTemplate; - private JdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) { + private final JdbcChatMemoryDialect dialect; + + private JdbcChatMemoryRepository(JdbcTemplate jdbcTemplate, JdbcChatMemoryDialect dialect) { Assert.notNull(jdbcTemplate, "jdbcTemplate cannot be null"); + Assert.notNull(dialect, "dialect cannot be null"); this.jdbcTemplate = jdbcTemplate; + this.dialect = dialect; } @Override public List findConversationIds() { - List conversationIds = this.jdbcTemplate.query(QUERY_GET_IDS, rs -> { + List conversationIds = this.jdbcTemplate.query(dialect.getSelectConversationIdsSql(), rs -> { var ids = new ArrayList(); while (rs.next()) { ids.add(rs.getString(1)); @@ -84,7 +75,7 @@ public class JdbcChatMemoryRepository implements ChatMemoryRepository { @Override public List findByConversationId(String conversationId) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); - return this.jdbcTemplate.query(QUERY_GET, new MessageRowMapper(), conversationId); + return this.jdbcTemplate.query(dialect.getSelectMessagesSql(), new MessageRowMapper(), conversationId); } @Override @@ -93,13 +84,14 @@ public class JdbcChatMemoryRepository implements ChatMemoryRepository { Assert.notNull(messages, "messages cannot be null"); Assert.noNullElements(messages, "messages cannot contain null elements"); this.deleteByConversationId(conversationId); - this.jdbcTemplate.batchUpdate(QUERY_ADD, new AddBatchPreparedStatement(conversationId, messages)); + this.jdbcTemplate.batchUpdate(dialect.getInsertMessageSql(), + new AddBatchPreparedStatement(conversationId, messages)); } @Override public void deleteByConversationId(String conversationId) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); - this.jdbcTemplate.update(QUERY_CLEAR, conversationId); + this.jdbcTemplate.update(dialect.getDeleteMessagesSql(), conversationId); } private record AddBatchPreparedStatement(String conversationId, List messages, @@ -154,6 +146,8 @@ public class JdbcChatMemoryRepository implements ChatMemoryRepository { private JdbcTemplate jdbcTemplate; + private JdbcChatMemoryDialect dialect; + private Builder() { } @@ -162,8 +156,15 @@ public class JdbcChatMemoryRepository implements ChatMemoryRepository { return this; } + public Builder dialect(JdbcChatMemoryDialect dialect) { + this.dialect = dialect; + return this; + } + public JdbcChatMemoryRepository build() { - return new JdbcChatMemoryRepository(this.jdbcTemplate); + if (this.dialect == null) + throw new IllegalStateException("Dialect must be set"); + return new JdbcChatMemoryRepository(this.jdbcTemplate, this.dialect); } } diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/MysqlChatMemoryDialect.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/MysqlChatMemoryDialect.java new file mode 100644 index 000000000..8bc7d2c21 --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/MysqlChatMemoryDialect.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.chat.memory.jdbc; + +/** + * Dialect for MySQL. + * + * @author Mark Pollack + * @since 1.0.0 + */ +public class MysqlChatMemoryDialect implements JdbcChatMemoryDialect { + + @Override + public String getSelectMessagesSql() { + return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY `timestamp` DESC LIMIT ?"; + } + + @Override + public String getInsertMessageSql() { + return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, `timestamp`) VALUES (?, ?, ?, ?)"; + } + + @Override + public String getSelectConversationIdsSql() { + return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY"; + } + + @Override + public String getDeleteMessagesSql() { + return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?"; + } + +} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/PostgresChatMemoryDialect.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/PostgresChatMemoryDialect.java new file mode 100644 index 000000000..424eb1aed --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/PostgresChatMemoryDialect.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.chat.memory.jdbc; + +/** + * Dialect for Postgres. + * + * @author Mark Pollack + * @since 1.0.0 + */ +public class PostgresChatMemoryDialect implements JdbcChatMemoryDialect { + + @Override + public String getSelectMessagesSql() { + return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY \"timestamp\""; + } + + @Override + public String getInsertMessageSql() { + return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, \"timestamp\") VALUES (?, ?, ?, ?)"; + } + + @Override + public String getSelectConversationIdsSql() { + return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY"; + } + + @Override + public String getDeleteMessagesSql() { + return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?"; + } + +} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/SqlServerChatMemoryDialect.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/SqlServerChatMemoryDialect.java new file mode 100644 index 000000000..48704a5f1 --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/SqlServerChatMemoryDialect.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.chat.memory.jdbc; + +/** + * Dialect for SQL Server. + * + * @author Mark Pollack + * @since 1.0.0 + */ +public class SqlServerChatMemoryDialect implements JdbcChatMemoryDialect { + + @Override + public String getSelectMessagesSql() { + return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY [timestamp] ASC"; + } + + @Override + public String getInsertMessageSql() { + return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, [timestamp]) VALUES (?, ?, ?, ?)"; + } + + @Override + public String getSelectConversationIdsSql() { + return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY"; + } + + @Override + public String getDeleteMessagesSql() { + return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?"; + } + +} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRepositoryRuntimeHints.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRepositoryRuntimeHints.java index 3b518733f..3ce817185 100644 --- a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRepositoryRuntimeHints.java +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRepositoryRuntimeHints.java @@ -34,9 +34,7 @@ class JdbcChatMemoryRepositoryRuntimeHints implements RuntimeHintsRegistrar { hints.reflection() .registerType(DataSource.class, hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); - hints.resources() - .registerPattern("org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql") - .registerPattern("org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql"); + hints.resources().registerPattern("org/springframework/ai/chat/memory/jdbc/schema-*.sql"); } } diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-hsqldb.sql b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-hsqldb.sql new file mode 100644 index 000000000..da29e6b3b --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-hsqldb.sql @@ -0,0 +1,10 @@ +CREATE TABLE SPRING_AI_CHAT_MEMORY ( + conversation_id VARCHAR(36) NOT NULL, + content LONGVARCHAR NOT NULL, + type VARCHAR(10) NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, timestamp DESC); + +ALTER TABLE SPRING_AI_CHAT_MEMORY ADD CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')); diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql index eb17a22be..6a0ef8755 100644 --- a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql @@ -1,10 +1,10 @@ -CREATE TABLE IF NOT EXISTS ai_chat_memory ( +CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(10) NOT NULL, `timestamp` TIMESTAMP NOT NULL, - CONSTRAINT type_check CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) + CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) ); -CREATE INDEX IF NOT EXISTS ai_chat_memory_conversation_id_timestamp_idx -ON ai_chat_memory(conversation_id, `timestamp`); \ No newline at end of file +CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX +ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`); \ No newline at end of file diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql index 3061e8a15..0733cbde5 100644 --- a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql @@ -1,9 +1,9 @@ -CREATE TABLE IF NOT EXISTS ai_chat_memory ( +CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(10) NOT NULL CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')), "timestamp" TIMESTAMP NOT NULL ); -CREATE INDEX IF NOT EXISTS ai_chat_memory_conversation_id_timestamp_idx -ON ai_chat_memory(conversation_id, "timestamp"); \ No newline at end of file +CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX +ON SPRING_AI_CHAT_MEMORY(conversation_id, "timestamp"); \ No newline at end of file diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-sqlserver.sql b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-sqlserver.sql new file mode 100644 index 000000000..1053e7d5b --- /dev/null +++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-sqlserver.sql @@ -0,0 +1,9 @@ +CREATE TABLE SPRING_AI_CHAT_MEMORY ( + conversation_id VARCHAR(36) NOT NULL, + content NVARCHAR(MAX) NOT NULL, + type VARCHAR(10) NOT NULL, + [timestamp] DATETIME2 NOT NULL DEFAULT SYSDATETIME(), + CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) +); + +CREATE INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, [timestamp] DESC); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc index 3b8253c9f..a0d467019 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc @@ -5,7 +5,7 @@ Large language models (LLMs) are stateless, meaning they do not retain informati The `ChatMemory` abstraction allows you to implement various types of memory to support different use cases. The underlying storage of the messages is handled by the `ChatMemoryRepository`, whose sole responsibility is to store and retrieve messages. It's up to the `ChatMemory` implementation to decide which messages to keep and when to remove them. Examples of strategies could include keeping the last N messages, keeping messages for a certain time period, or keeping messages up to a certain token limit. -Before choosing a memory type, it's important to understand the difference between chat memory and chat history. +Before choosing a memory type, it's essential to understand the difference between chat memory and chat history. * *Chat Memory*. The information that a large-language model retains and uses to maintain contextual awareness throughout a conversation. * *Chat History*. The entire conversation history, including all messages exchanged between the user and the model. @@ -66,7 +66,7 @@ ChatMemoryRepository repository = new InMemoryChatMemoryRepository(); === JdbcChatMemoryRepository -`JdbcChatMemoryRepository` is a built-in implementation that uses JDBC to store messages in a relational database. It is suitable for applications that require persistent storage of chat memory. +`JdbcChatMemoryRepository` is a built-in implementation that uses JDBC to store messages in a relational database. It supports multiple databases out-of-the-box and is suitable for applications that require persistent storage of chat memory. First, add the following dependency to your project: @@ -105,12 +105,13 @@ ChatMemory chatMemory = MessageWindowChatMemory.builder() .build(); ---- -If you'd rather create the `JdbcChatMemoryRepository` manually, you can do so by providing a `JdbcTemplate` instance: +If you'd rather create the `JdbcChatMemoryRepository` manually, you can do so by providing a `JdbcTemplate` instance and optionally a custom `JdbcChatMemoryDialect`: [source,java] ---- ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder() .jdbcTemplate(jdbcTemplate) + .dialect(new PostgresChatMemoryDialect()) // Optional: custom dialect, auto-detected by default .build(); ChatMemory chatMemory = MessageWindowChatMemory.builder() @@ -119,22 +120,57 @@ ChatMemory chatMemory = MessageWindowChatMemory.builder() .build(); ---- +==== Supported Databases and Dialect Abstraction + +Spring AI supports multiple relational databases via a dialect abstraction. The following databases are supported out-of-the-box: + +- PostgreSQL +- MySQL / MariaDB +- SQL Server +- HSQLDB + +The correct dialect is auto-detected from the JDBC URL. You can extend support for other databases by implementing the `JdbcChatMemoryDialect` interface. + ==== Configuration Properties [cols="2,5,1",stripes=even] |=== |Property | Description | Default Value -| `spring.ai.chat.memory.repository.jdbc.initialize-schema` | Whether to initialize the schema on startup. | `true` +| `spring.ai.chat.memory.repository.jdbc.initialize-schema` | Controls when to initialize the schema. Values: `embedded` (default), `always`, `never`. | `embedded` +| `spring.ai.chat.memory.repository.jdbc.schema` | Location of the schema script to use for initialization. Supports `classpath:` URLs and platform placeholders. | `classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql` |=== - ==== Schema Initialization -The auto-configuration will automatically create the `ai_chat_memory` table using the JDBC driver. Currently, only PostgreSQL and MariaDB are supported. +The auto-configuration will automatically create the `SPRING_AI_CHAT_MEMORY` table on startup, using a vendor-specific SQL script for your database. By default, schema initialization runs only for embedded databases (H2, HSQL, Derby, etc.). -You can disable the schema initialization by setting the property `spring.ai.chat.memory.repository.jdbc.initialize-schema` to `false`. +You can control schema initialization using the `spring.ai.chat.memory.repository.jdbc.initialize-schema` property: -If your project uses a tool like Flyway or Liquibase to manage your database schemas, you can disable the schema initialization and refer to link:https://github.com/spring-projects/spring-ai/tree/main/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc[these SQL scripts] for configuring those tools to create the `ai_chat_memory` table. +[source,properties] +---- +spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded # Only for embedded DBs (default) +spring.ai.chat.memory.repository.jdbc.initialize-schema=always # Always initialize +spring.ai.chat.memory.repository.jdbc.initialize-schema=never # Never initialize (useful with Flyway/Liquibase) +---- + +To override the schema script location, use: + +[source,properties] +---- +spring.ai.chat.memory.repository.jdbc.schema=classpath:/custom/path/schema-mysql.sql +---- + +==== Extending Dialects + +To add support for a new database, implement the `JdbcChatMemoryDialect` interface and provide SQL for selecting, inserting, and deleting messages. You can then pass your custom dialect to the repository builder. + +[source,java] +---- +ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder() + .jdbcTemplate(jdbcTemplate) + .dialect(new MyCustomDbDialect()) + .build(); +---- === Neo4j ChatMemoryRepository