/* * Copyright (C) 2015 RankSys http://ranksys.org * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package es.uam.eps.ir.ranksys.fast.preference; import es.uam.eps.ir.ranksys.core.preference.IdPref; import it.unimi.dsi.fastutil.doubles.DoubleIterator; import it.unimi.dsi.fastutil.ints.IntIterator; import org.jooq.*; import org.jooq.impl.DSL; import org.ranksys.core.preference.MutablePreferenceData; import org.ranksys.core.util.iterators.StreamDoubleIterator; import org.ranksys.core.util.iterators.StreamIntIterator; import javax.sql.DataSource; import java.util.stream.IntStream; import java.util.stream.Stream; import static org.jooq.impl.DSL.*; /** * SQL-backed preference data. * * @author Saúl Vargas (Saul@VargasSandoval.es) */ public class SQLPreferenceData implements FastPreferenceData<String, String>, MutablePreferenceData<String, String> { private final static Table<Record> USERS = DSL.table(name("USERS")); private final static Field<String> USER_ID = DSL.field(name("USER_ID"), String.class); private final static Field<Integer> UIDX = DSL.field(name("UIDX"), int.class); private final static Table<Record> ITEMS = DSL.table(name("ITEMS")); private final static Field<String> ITEM_ID = DSL.field(name("ITEM_ID"), String.class); private final static Field<Integer> IIDX = DSL.field(name("IIDX"), int.class); private final static Field<Double> V = DSL.field(name("V"), double.class); private final static Sequence<Integer> SEQ_UIDX = DSL.sequence(name("SEQ_UIDX"), int.class); private final static Sequence<Integer> SEQ_IIDX = DSL.sequence(name("SEQ_IIDX"), int.class); private final DSLContext dsl; private final Table<Record> DATA; /** * Constructor. * * @param ds datasource * @param dialect SQL dialect * @param table table storing the preferences (train, test, etc.) */ public SQLPreferenceData(DataSource ds, SQLDialect dialect, String table) { this.dsl = DSL.using(ds, dialect); this.DATA = DSL.table(name(table.toUpperCase())); } @Override public int numUsers(int iidx) { return dsl .selectCount() .from(DATA) .where(IIDX.eq(iidx)) .fetchOne().value1(); } @Override public int numItems(int uidx) { return dsl .selectCount() .from(DATA) .where(UIDX.eq(uidx)) .fetchOne().value1(); } @Override public IntStream getUidxWithPreferences() { return dsl .selectDistinct(UIDX) .from(DATA) .fetch().stream() .mapToInt(Record1::value1); } @Override public IntStream getIidxWithPreferences() { return dsl .selectDistinct(IIDX) .from(DATA) .fetch().stream() .mapToInt(Record1::value1); } @Override public Stream<IdxPref> getUidxPreferences(int uidx) { return dsl .select(IIDX, V) .from(DATA) .where(UIDX.eq(uidx)) .fetch().stream() .map(r -> new IdxPref(r.value1(), r.value2())); } @Override public Stream<IdxPref> getIidxPreferences(int iidx) { return dsl .select(UIDX, V) .from(DATA) .where(IIDX.eq(iidx)) .fetch().stream() .map(r -> new IdxPref(r.value1(), r.value2())); } @Override public int numUsersWithPreferences() { return dsl .select(DSL.countDistinct(UIDX)) .from(DATA) .fetchOne().value1(); } @Override public int numItemsWithPreferences() { return dsl .select(DSL.countDistinct(IIDX)) .from(DATA) .fetchOne().value1(); } @Override public int numUsers(String i) { return dsl .selectCount() .from(DATA).naturalJoin(ITEMS) .where(ITEM_ID.eq(i)) .fetchOne().value1(); } @Override public int numItems(String u) { return dsl .selectCount() .from(DATA).naturalJoin(USERS) .where(USER_ID.eq(u)).fetchOne().value1(); } @Override public int numPreferences() { return dsl .selectCount() .from(DATA) .fetchOne().value1(); } @Override public Stream<String> getUsersWithPreferences() { return dsl .selectDistinct(USER_ID) .from(DATA).naturalJoin(USERS) .fetch().stream().map(Record1::value1); } @Override public Stream<String> getItemsWithPreferences() { return dsl .selectDistinct(ITEM_ID) .from(DATA).naturalJoin(ITEMS) .fetch().stream() .map(Record1::value1); } @Override public Stream<IdPref<String>> getUserPreferences(String u) { return dsl.select(ITEM_ID, V) .from(DATA).naturalJoin(USERS).naturalJoin(ITEMS) .where(USER_ID.eq(u)) .fetch().stream() .map(r -> new IdPref<>(r.value1(), r.value2())); } @Override public Stream<IdPref<String>> getItemPreferences(String i) { return dsl .select(USER_ID, V) .from(DATA).naturalJoin(USERS).naturalJoin(ITEMS) .where(ITEM_ID.eq(i)) .fetch().stream() .map(r -> new IdPref<>(r.value1(), r.value2())); } @Override public boolean containsUser(String u) { return dsl .select() .from(USERS) .where(USER_ID.eq(u)) .fetch().isNotEmpty(); } @Override public int numUsers() { return dsl .selectCount() .from(USERS) .fetchOne().value1(); } @Override public Stream<String> getAllUsers() { return dsl .select(USER_ID) .from(USERS) .fetch().stream() .map(Record1::value1); } @Override public boolean containsItem(String i) { return dsl .select() .from(ITEMS) .where(ITEM_ID.eq(i)) .fetch().isNotEmpty(); } @Override public int numItems() { return dsl .selectCount() .from(ITEMS) .fetchOne().value1(); } @Override public Stream<String> getAllItems() { return dsl .select(ITEM_ID) .from(ITEMS) .fetch().stream() .map(Record1::value1); } @Override public int user2uidx(String u) { return dsl .select(UIDX) .from(USERS) .where(USER_ID.eq(u)) .fetchOne(UIDX); } @Override public String uidx2user(int uidx) { return dsl .select(USER_ID) .from(USERS) .where(UIDX.eq(uidx)) .fetchOne(USER_ID); } @Override public IntStream getAllUidx() { return dsl .select(UIDX) .from(USERS) .fetch().stream() .mapToInt(Record1::value1); } @Override public int item2iidx(String i) { return dsl .select(IIDX) .from(ITEMS) .where(ITEM_ID.eq(i)) .fetchOne(IIDX); } @Override public String iidx2item(int iidx) { return dsl .select(ITEM_ID) .from(ITEMS) .where(IIDX.eq(iidx)) .fetchOne(ITEM_ID); } @Override public IntStream getAllIidx() { return dsl .select(IIDX) .from(ITEMS) .fetch().stream() .mapToInt(Record1::value1); } @Override public boolean addUser(String u) { return dsl .insertInto(USERS, UIDX, USER_ID) .values(SEQ_UIDX.nextval(), val(u)) .execute() == 1; } @Override public boolean removeUser(String u) { return dsl .delete(USERS) .where(USER_ID.eq(u)) .execute() == 1; } @Override public boolean addItem(String i) { return dsl .insertInto(ITEMS, IIDX, ITEM_ID) .values(SEQ_IIDX.nextval(), val(i)) .execute() == 1; } @Override public boolean removeItem(String i) { return dsl .delete(ITEMS) .where(ITEM_ID.eq(i)) .execute() == 1; } @Override public boolean addPref(String u, String i, double v, Object n) { return dsl .insertInto(DATA, UIDX, IIDX, V) .values(user2uidx(u), item2iidx(i), v) .execute() == 1; } @Override public boolean removePref(String u, String i) { return dsl .delete(DATA) .where(UIDX.eq(user2uidx(u)).and(IIDX.eq(item2iidx(i)))) .execute() == 1; } @Override public IntIterator getUidxIidxs(int uidx) { return new StreamIntIterator(dsl .select(IIDX) .from(DATA) .where(UIDX.eq(uidx)) .fetch().stream() .mapToInt(Record1::value1)); } @Override public DoubleIterator getUidxVs(int uidx) { return new StreamDoubleIterator(dsl .select(V) .from(DATA) .where(UIDX.eq(uidx)) .fetch().stream() .mapToDouble(Record1::value1)); } @Override public IntIterator getIidxUidxs(int iidx) { return new StreamIntIterator(dsl .select(UIDX) .from(DATA) .where(IIDX.eq(iidx)) .fetch().stream() .mapToInt(Record1::value1)); } @Override public DoubleIterator getIidxVs(int iidx) { return new StreamDoubleIterator(dsl .select(V) .from(DATA) .where(IIDX.eq(iidx)) .fetch().stream() .mapToDouble(Record1::value1)); } @Override public boolean useIteratorsPreferentially() { return false; } /** * Creates empty tables for their use in SQLPreferenceData * * @param ds datasource * @param dialect SQL dialect * @param tables tables for preferences to be created (train, test, etc.) */ public static void create(DataSource ds, SQLDialect dialect, String... tables) { DSLContext dsl = DSL.using(ds, dialect); dsl.dropTableIfExists(USERS) .execute(); dsl.createTable(USERS) .column(UIDX, UIDX.getDataType().nullable(false)) .column(USER_ID, USER_ID.getDataType().nullable(false)) .execute(); dsl.alterTable(USERS) .add(constraint("PK_USERS").primaryKey(UIDX)) .execute(); dsl.dropSequenceIfExists(SEQ_UIDX) .execute(); dsl.createSequence(SEQ_UIDX) .execute(); dsl.dropTableIfExists(ITEMS) .execute(); dsl.createTable(ITEMS) .column(IIDX, IIDX.getDataType().nullable(false)) .column(ITEM_ID, ITEM_ID.getDataType().nullable(false)) .execute(); dsl.alterTable(ITEMS) .add(constraint("PK_ITEMS").primaryKey(IIDX)) .execute(); dsl.dropSequenceIfExists(SEQ_IIDX) .execute(); dsl.createSequence(SEQ_IIDX) .execute(); for (String table : tables) { Table<Record> DATA = DSL.table(name(table.toUpperCase())); dsl.dropTableIfExists(DATA) .execute(); dsl.createTable(DATA) .column(UIDX, UIDX.getDataType().nullable(false)) .column(IIDX, IIDX.getDataType().nullable(false)) .column(V, V.getDataType().nullable(false)) .execute(); dsl.alterTable(DATA) .add(constraint("PK_" + table).primaryKey(UIDX, IIDX)) .execute(); dsl.alterTable(DATA) .add(constraint("FK_" + table + "_uidx") .foreignKey(UIDX).references(USERS, UIDX).onDeleteCascade()) .execute(); dsl.alterTable(DATA) .add(constraint("FK_" + table + "_iidx") .foreignKey(IIDX).references(ITEMS, IIDX).onDeleteCascade()) .execute(); } } }