<commit_before>package dev.kkorolyov.sqlob.persistence;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dev.kkorolyov.sqlob.annotation.Table;
import dev.kkorolyov.sqlob.logging.Logger;

/**
 * Manages persistence at the class/table level.
 * @param <T> persisted class type
 */
final class SqlobClass<T> {
	private static final Logger log = Logger.getLogger(SqlobClass.class.getName());
	
	final Class<T> c;
	final Constructor<T> constructor;
	final String name;
	final Iterable<SqlobField> fields;
	private final String 	insert,
												fieldSelection;
	
	SqlobClass(Class<T> c, Iterable<SqlobField> fields, Connection conn) throws SQLException {
		this.c = c;
		this.fields = fields;

		try {
			constructor = this.c.getDeclaredConstructor();
			constructor.setAccessible(true);
		} catch (NoSuchMethodException e) {
			throw new NonPersistableException(c.getName() + " does not provide a nullary constructor");
		}
		Table override = this.c.getAnnotation(Table.class);
		if (override == null) name = c.getSimpleName();
		else if (override.value().length() <= 0) throw new NonPersistableException(c.getName() + " has table override with empty name");
		else name = override.value();

		StringBuilder createBuilder = new StringBuilder("CREATE TABLE IF NOT EXISTS ").append(name).append("(").append(Constants.ID_NAME).append(" ").append(Constants.ID_TYPE).append(" PRIMARY KEY");
		StringBuilder insertBuilder = new StringBuilder("INSERT INTO ").append(name).append(" (").append(Constants.ID_NAME);
		StringBuilder	valuesBuilder = new StringBuilder("VALUES (?");
		StringBuilder fieldSelectionBuilder = new StringBuilder();

		for (SqlobField field : fields) {
			createBuilder.append(", ").append(field.getInit(Constants.ID_NAME));
			
			insertBuilder.append(", ").append(field.name);
			valuesBuilder.append(", ?");
			
			if (fieldSelectionBuilder.length() > 0) fieldSelectionBuilder.append(", ");
			fieldSelectionBuilder.append(field.name);
		}
		createBuilder.append(")");
		
		valuesBuilder.append(")");
		insertBuilder.append(") ").append(valuesBuilder);
		
		insert = insertBuilder.toString();
		fieldSelection = fieldSelectionBuilder.toString();

		String create = createBuilder.toString();
		try (Statement s = conn.createStatement()) {
			s.executeUpdate(create);
		}
		log.debug(() -> "Initialized new SqlobClass: " + this + System.lineSeparator() + "\t" + create);
	}

	@SuppressWarnings("unchecked")
	UUID getId(Object instance, Connection conn) throws SQLException {
		Condition where = buildEquals((T) instance);
		String select = buildSelect(Constants.ID_NAME, where);
		
		try (PreparedStatement s = conn.prepareStatement(select)) {
			int counter = 1;
			for (Object value : where.values()) {
				if (!shortCircuitSet(s, counter++, value, conn))	// Missing reference
					return null;	// Short-circuit
			}
			try (ResultSet rs = s.executeQuery()) {
				UUID result = rs.next() ? UUID.fromString(rs.getString(1)) : null;
				
				log.debug(() -> (result == null ? "Failed to find " : "Found ") + "ID of " + this + " instance: " + instance + (result == null ? "" : "->" + result) + System.lineSeparator() + "\t" + applyStatement(select, (where == null ? null : where.values())));
				return result;
			}
		}
	}
	
	T get(UUID id, Connection conn) throws SQLException {
		Set<T> instances = get(new Condition(Constants.ID_NAME, "=", id.toString()), conn);
		T result = instances.isEmpty() ? null : instances.iterator().next();
		
		log.debug(() -> (result == null ? "Failed to find " : "Found ") + "instance of " + this + ": " + id + (result == null ? "" : "->" + result));
		return result;
	}
	Set<T> get(Condition where, Connection conn) throws SQLException {
		Set<T> results = new HashSet<>();
		String select = buildSelect(fieldSelection, where);
		
		try (PreparedStatement s = conn.prepareStatement(select)) {
			if (where != null) {	// Has conditions
				int counter = 1;
				for (Object value : where.values()) {
					if (!shortCircuitSet(s, counter++, value, conn))	// Missing reference
						return results;	// Short-circuit
				}
			}
			try (ResultSet rs = s.executeQuery()) {
				while(rs.next()) {
					try {
						T result = constructor.newInstance();
						
						for (SqlobField field : fields)
							field.apply(result, rs, conn);
						
						results.add(result);
					} catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException e) {
						throw new NonPersistableException("Failed to instantiate " + c.getName(), e);
					}
				}
			}
		}
		log.debug(() -> "Found " + results.size() + " instances of " + this + System.lineSeparator() + "\t" + applyStatement(select, (where == null ? null : where.values())));
		return results;
	}
	
	UUID put(Object instance, Connection conn) throws SQLException {
		UUID id = getId(instance, conn);
		if (id == null) {
			id = UUID.randomUUID();
			put(id, instance, conn);
		}
		return id;
	}
	boolean put(UUID id, Object instance, Connection conn) throws SQLException {
		boolean result = drop(id, conn);	// Drop any previous version
		List<Object> parameters = new LinkedList<>();	// For logging
		
		try (PreparedStatement s = conn.prepareStatement(insert)) {
			s.setString(1, id.toString());
			parameters.add(id);	// Logging

			int counter = 2;
			for (SqlobField field : fields) {
				Object value = 	field.get(instance),
												transformed = transform(value, conn);
				if (value != null && transformed == null)	// Missing reference
					transformed = field.put(value, conn);	// Recursive put

				s.setObject(counter++, transformed, field.typeCode);
				parameters.add(transformed);	// Logging
			}
			s.executeUpdate();
		}
		UUID finalId = id;
		
		log.debug(() -> (result ? "Replaced " : "Saved new ") + this + ": " + instance + "->" + finalId + System.lineSeparator() + "\t" + applyStatement(insert, parameters));
		return result;
	}
	
	boolean drop(UUID id, Connection conn) throws SQLException {
		boolean result = drop(new Condition(Constants.ID_NAME, "=", id.toString()), conn) > 0;
		<commit_msg>Replace StringBuilder craziness with Streams, Stringjoins<commit_after>package dev.kkorolyov.sqlob.persistence;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import dev.kkorolyov.sqlob.annotation.Table;
import dev.kkorolyov.sqlob.logging.Logger;

/**
 * Manages persistence at the class/table level.
 * @param <T> persisted class type
 */
final class SqlobClass<T> {
	private static final Logger log = Logger.getLogger(SqlobClass.class.getName());

	final String name;
	private final Class<T> c;
	private final Constructor<T> constructor;
	private final Iterable<SqlobField> fields;
	private final String insert;
	private final String fieldSelection;
	
	SqlobClass(Class<T> c, Iterable<SqlobField> fields, Connection conn) throws SQLException {
		this.c = c;
		this.fields = fields;

		try {
			constructor = this.c.getDeclaredConstructor();
			constructor.setAccessible(true);
		} catch (NoSuchMethodException e) {
			throw new NonPersistableException(c.getName() + " does not provide a nullary constructor");
		}
		Table override = this.c.getAnnotation(Table.class);
		if (override == null) name = c.getSimpleName();
		else if (override.value().length() <= 0) throw new NonPersistableException(c.getName() + " has table override with empty name");
		else name = override.value();

		fieldSelection = StreamSupport.stream(fields.spliterator(), true)	// Used in SELECT clause
																	.map(field -> field.name)
																	.collect(Collectors.joining(", "));
		insert = "INSERT INTO " + name + " (" + Constants.ID_NAME + fieldSelection + ") "
						 + StreamSupport.stream(fields.spliterator(), false)
														.map(field -> "?")
														.collect(Collectors.joining(", ", "VALUES (?", ")"));
		String create = "CREATE TABLE IF NOT EXISTS " + name + "(" + Constants.ID_NAME + " " + Constants.ID_TYPE + " PRIMARY KEY, "
										+ StreamSupport.stream(fields.spliterator(), false)
																	 .map(field -> field.getInit(Constants.ID_NAME))
																	 .collect(Collectors.joining(", "));
		try (Statement s = conn.createStatement()) {
			s.executeUpdate(create);
		}
		log.debug(() -> "Initialized new SqlobClass: " + this + System.lineSeparator() + "\t" + create);
	}

	@SuppressWarnings("unchecked")
	UUID getId(Object instance, Connection conn) throws SQLException {
		Condition where = buildEquals((T) instance);
		String select = buildSelect(Constants.ID_NAME, where);
		
		try (PreparedStatement s = conn.prepareStatement(select)) {
			int counter = 1;
			for (Object value : where.values()) {
				if (!shortCircuitSet(s, counter++, value, conn))	// Missing reference
					return null;	// Short-circuit
			}
			try (ResultSet rs = s.executeQuery()) {
				UUID result = rs.next() ? UUID.fromString(rs.getString(1)) : null;
				
				log.debug(() -> (result == null ? "Failed to find " : "Found ") + "ID of " + this + " instance: " + instance + (result == null ? "" : "->" + result) + System.lineSeparator() + "\t" + applyStatement(select, (where == null ? null : where.values())));
				return result;
			}
		}
	}
	
	T get(UUID id, Connection conn) throws SQLException {
		Set<T> instances = get(new Condition(Constants.ID_NAME, "=", id.toString()), conn);
		T result = instances.isEmpty() ? null : instances.iterator().next();
		
		log.debug(() -> (result == null ? "Failed to find " : "Found ") + "instance of " + this + ": " + id + (result == null ? "" : "->" + result));
		return result;
	}
	Set<T> get(Condition where, Connection conn) throws SQLException {
		Set<T> results = new HashSet<>();
		String select = buildSelect(fieldSelection, where);
		
		try (PreparedStatement s = conn.prepareStatement(select)) {
			if (where != null) {	// Has conditions
				int counter = 1;
				for (Object value : where.values()) {
					if (!shortCircuitSet(s, counter++, value, conn))	// Missing reference
						return results;	// Short-circuit
				}
			}
			try (ResultSet rs = s.executeQuery()) {
				while(rs.next()) {
					try {
						T result = constructor.newInstance();
						
						for (SqlobField field : fields)
							field.apply(result, rs, conn);
						
						results.add(result);
					} catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException e) {
						throw new NonPersistableException("Failed to instantiate " + c.getName(), e);
					}
				}
			}
		}
		log.debug(() -> "Found " + results.size() + " instances of " + this + System.lineSeparator() + "\t" + applyStatement(select, (where == null ? null : where.values())));
		return results;
	}
	
	UUID put(Object instance, Connection conn) throws SQLException {
		UUID id = getId(instance, conn);
		if (id == null) {
			id = UUID.randomUUID();
			put(id, instance, conn);
		}
		return id;
	}
	boolean put(UUID id, Object instance, Connection conn) throws SQLException {
		boolean result = drop(id, conn);	// Drop any previous version
		List<Object> parameters = new LinkedList<>();	// For logging
		
		try (PreparedStatement s = conn.prepareStatement(insert)) {
			s.setString(1, id.toString());
			parameters.add(id);	// Logging

			int counter = 2;
			for (SqlobField field : fields) {
				Object value = 	field.get(instance),
												transformed = transform(value, conn);
				if (value != null && transformed == null)	// Missing reference
					transformed = field.put(value, conn);	// Recursive put

				s.setObject(counter++, transformed, field.typeCode);
				parameters.add(transformed);	// Logging
			}
			s.executeUpdate();
		}
		log.debug(() -> (result ? "Replaced " : "Saved new ") + this + ": " + instance + "->" + id + System.lineSeparator() + "\t" + applyStatement(insert, parameters));
		return result;
	}
	
	boolean drop(UUID id, Connection conn) throws SQLException {
		boolean result = drop(new Condition(Constants.ID_NAME, "=", id.toString()), conn) > 0;
		
{'fraction_non_alphanumeric': 0.14636534225340622, 'fraction_numerical': 0.001223790487068614, 'mean_word_length': 3.696551724137931, 'pattern_counts': {'":': 4, '<': 31, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '130444', 'n_tokens_mistral': 4243, 'n_tokens_neox': 3623, 'n_words': 950}