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>
This commit is contained in:
committed by
Ilayaperumal Gopinathan
parent
6ae1c13aee
commit
08958af670
@@ -78,6 +78,23 @@
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mssqlserver</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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<String> resolveSchemaLocations(DataSource dataSource) {
|
||||
var platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
|
||||
|
||||
return platformResolver.resolveAll(dataSource, SCHEMA_LOCATION);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> schemas = jdbcTemplate.queryForList("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA",
|
||||
String.class);
|
||||
System.out.println("Available schemas: " + schemas);
|
||||
|
||||
List<String> 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<java.net.URL> 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.<Message>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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.<Message>of(new UserMessage("Message from the user 1"),
|
||||
new AssistantMessage("Message from the assistant 1"));
|
||||
var multipleMessages = List.<Message>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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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.<Message>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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'));
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> findConversationIds() {
|
||||
List<String> conversationIds = this.jdbcTemplate.query(QUERY_GET_IDS, rs -> {
|
||||
List<String> conversationIds = this.jdbcTemplate.query(dialect.getSelectConversationIdsSql(), rs -> {
|
||||
var ids = new ArrayList<String>();
|
||||
while (rs.next()) {
|
||||
ids.add(rs.getString(1));
|
||||
@@ -84,7 +75,7 @@ public class JdbcChatMemoryRepository implements ChatMemoryRepository {
|
||||
@Override
|
||||
public List<Message> 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<Message> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = ?";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = ?";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = ?";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
@@ -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`);
|
||||
CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
|
||||
ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`);
|
||||
@@ -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");
|
||||
CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
|
||||
ON SPRING_AI_CHAT_MEMORY(conversation_id, "timestamp");
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user