wenzelm@63788: /* Title: Pure/General/sql.scala wenzelm@63778: Author: Makarius wenzelm@63778: wenzelm@65006: Support for SQL databases: SQLite and PostgreSQL. wenzelm@63778: */ wenzelm@63778: wenzelm@63778: package isabelle wenzelm@63778: wenzelm@69393: wenzelm@65021: import java.time.OffsetDateTime wenzelm@65021: import java.sql.{DriverManager, Connection, PreparedStatement, ResultSet} wenzelm@63779: wenzelm@65740: import scala.collection.mutable wenzelm@65740: wenzelm@63779: wenzelm@63778: object SQL wenzelm@63778: { wenzelm@65006: /** SQL language **/ wenzelm@65006: wenzelm@65730: type Source = String wenzelm@65730: wenzelm@65730: wenzelm@63778: /* concrete syntax */ wenzelm@63778: wenzelm@65321: def escape_char(c: Char): String = wenzelm@63778: c match { wenzelm@63778: case '\u0000' => "\\0" wenzelm@63778: case '\'' => "\\'" wenzelm@63778: case '\"' => "\\\"" wenzelm@63778: case '\b' => "\\b" wenzelm@63778: case '\n' => "\\n" wenzelm@63778: case '\r' => "\\r" wenzelm@63778: case '\t' => "\\t" wenzelm@63778: case '\u001a' => "\\Z" wenzelm@63778: case '\\' => "\\\\" wenzelm@63778: case _ => c.toString wenzelm@63778: } wenzelm@63778: wenzelm@65730: def string(s: String): Source = wenzelm@69327: s.iterator.map(escape_char(_)).mkString("'", "", "'") wenzelm@63778: wenzelm@65730: def ident(s: String): Source = wenzelm@65677: Long_Name.implode(Long_Name.explode(s).map(a => quote(a.replace("\"", "\"\"")))) wenzelm@63779: wenzelm@65730: def enclose(s: Source): Source = "(" + s + ")" wenzelm@65730: def enclosure(ss: Iterable[Source]): Source = ss.mkString("(", ", ", ")") wenzelm@63791: wenzelm@65774: def select(columns: List[Column] = Nil, distinct: Boolean = false): Source = wenzelm@65774: "SELECT " + (if (distinct) "DISTINCT " else "") + wenzelm@65774: (if (columns.isEmpty) "*" else commas(columns.map(_.ident))) + " FROM " wenzelm@65644: wenzelm@65775: val join_outer: Source = " LEFT OUTER JOIN " wenzelm@65775: val join_inner: Source = " INNER JOIN " wenzelm@65776: def join(outer: Boolean): Source = if (outer) join_outer else join_inner wenzelm@65775: wenzelm@65804: def member(x: Source, set: Iterable[String]): Source = wenzelm@65804: set.iterator.map(a => x + " = " + SQL.string(a)).mkString("(", " OR ", ")") wenzelm@65804: wenzelm@63779: wenzelm@65008: /* types */ wenzelm@65008: wenzelm@65008: object Type extends Enumeration wenzelm@65008: { wenzelm@65011: val Boolean = Value("BOOLEAN") wenzelm@65008: val Int = Value("INTEGER") wenzelm@65008: val Long = Value("BIGINT") wenzelm@65008: val Double = Value("DOUBLE PRECISION") wenzelm@65008: val String = Value("TEXT") wenzelm@65008: val Bytes = Value("BLOB") wenzelm@65014: val Date = Value("TIMESTAMP WITH TIME ZONE") wenzelm@65008: } wenzelm@65008: wenzelm@65730: def sql_type_default(T: Type.Value): Source = T.toString wenzelm@65013: wenzelm@65730: def sql_type_sqlite(T: Type.Value): Source = wenzelm@65019: if (T == Type.Boolean) "INTEGER" wenzelm@65019: else if (T == Type.Date) "TEXT" wenzelm@65019: else sql_type_default(T) wenzelm@65013: wenzelm@65730: def sql_type_postgresql(T: Type.Value): Source = wenzelm@65019: if (T == Type.Bytes) "BYTEA" wenzelm@65019: else sql_type_default(T) wenzelm@65008: wenzelm@65008: wenzelm@63779: /* columns */ wenzelm@63779: wenzelm@63779: object Column wenzelm@63779: { wenzelm@65280: def bool(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.Boolean, strict, primary_key) wenzelm@65280: def int(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.Int, strict, primary_key) wenzelm@65280: def long(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.Long, strict, primary_key) wenzelm@65280: def double(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.Double, strict, primary_key) wenzelm@65280: def string(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.String, strict, primary_key) wenzelm@65280: def bytes(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.Bytes, strict, primary_key) wenzelm@65280: def date(name: String, strict: Boolean = false, primary_key: Boolean = false): Column = wenzelm@65018: Column(name, Type.Date, strict, primary_key) wenzelm@63779: } wenzelm@63779: wenzelm@65018: sealed case class Column( wenzelm@65780: name: String, T: Type.Value, strict: Boolean = false, primary_key: Boolean = false, wenzelm@65780: expr: SQL.Source = "") wenzelm@63779: { wenzelm@66857: def make_primary_key: Column = copy(primary_key = true) wenzelm@66857: wenzelm@65691: def apply(table: Table): Column = wenzelm@65691: Column(Long_Name.qualify(table.name, name), T, strict = strict, primary_key = primary_key) wenzelm@65644: wenzelm@65780: def ident: Source = wenzelm@65780: if (expr == "") SQL.ident(name) wenzelm@65780: else enclose(expr) + " AS " + SQL.ident(name) wenzelm@65695: wenzelm@65730: def decl(sql_type: Type.Value => Source): Source = wenzelm@65695: ident + " " + sql_type(T) + (if (strict || primary_key) " NOT NULL" else "") wenzelm@63781: wenzelm@66856: def defined: String = ident + " IS NOT NULL" wenzelm@66856: def undefined: String = ident + " IS NULL" wenzelm@66856: wenzelm@68091: def equal(s: String): Source = ident + " = " + string(s) wenzelm@68091: def where_equal(s: String): Source = "WHERE " + equal(s) wenzelm@65593: wenzelm@65730: override def toString: Source = ident wenzelm@63779: } wenzelm@63779: wenzelm@63780: wenzelm@63780: /* tables */ wenzelm@63780: wenzelm@65730: sealed case class Table(name: String, columns: List[Column], body: Source = "") wenzelm@63780: { wenzelm@63790: private val columns_index: Map[String, Int] = wenzelm@63790: columns.iterator.map(_.name).zipWithIndex.toMap wenzelm@63790: wenzelm@63781: Library.duplicates(columns.map(_.name)) match { wenzelm@63781: case Nil => wenzelm@63781: case bad => error("Duplicate column names " + commas_quote(bad) + " for table " + quote(name)) wenzelm@63781: } wenzelm@63781: wenzelm@65730: def ident: Source = SQL.ident(name) wenzelm@65691: wenzelm@65730: def query: Source = wenzelm@65696: if (body == "") error("Missing SQL body for table " + quote(name)) wenzelm@65696: else SQL.enclose(body) wenzelm@65649: wenzelm@65778: def query_named: Source = query + " AS " + SQL.ident(name) wenzelm@65702: wenzelm@65730: def create(strict: Boolean = false, sql_type: Type.Value => Source): Source = wenzelm@65325: { wenzelm@65325: val primary_key = wenzelm@65325: columns.filter(_.primary_key).map(_.name) match { wenzelm@65325: case Nil => Nil wenzelm@65325: case keys => List("PRIMARY KEY " + enclosure(keys)) wenzelm@65325: } wenzelm@65701: "CREATE TABLE " + (if (strict) "" else "IF NOT EXISTS ") + wenzelm@65701: ident + " " + enclosure(columns.map(_.decl(sql_type)) ::: primary_key) wenzelm@63781: } wenzelm@63781: wenzelm@65698: def create_index(index_name: String, index_columns: List[Column], wenzelm@65730: strict: Boolean = false, unique: Boolean = false): Source = wenzelm@63791: "CREATE " + (if (unique) "UNIQUE " else "") + "INDEX " + wenzelm@65695: (if (strict) "" else "IF NOT EXISTS ") + SQL.ident(index_name) + " ON " + wenzelm@65695: ident + " " + enclosure(index_columns.map(_.name)) wenzelm@63791: wenzelm@65730: def insert_cmd(cmd: Source, sql: Source = ""): Source = wenzelm@65703: cmd + " INTO " + ident + " VALUES " + enclosure(columns.map(_ => "?")) + wenzelm@65698: (if (sql == "") "" else " " + sql) wenzelm@63791: wenzelm@65730: def insert(sql: Source = ""): Source = insert_cmd("INSERT", sql) wenzelm@65703: wenzelm@65730: def delete(sql: Source = ""): Source = wenzelm@65698: "DELETE FROM " + ident + wenzelm@65698: (if (sql == "") "" else " " + sql) wenzelm@65319: wenzelm@65774: def select( wenzelm@65774: select_columns: List[Column] = Nil, sql: Source = "", distinct: Boolean = false): Source = wenzelm@65698: SQL.select(select_columns, distinct = distinct) + ident + wenzelm@65698: (if (sql == "") "" else " " + sql) wenzelm@63790: wenzelm@65730: override def toString: Source = ident wenzelm@63780: } wenzelm@63790: wenzelm@63790: wenzelm@65012: wenzelm@65012: /** SQL database operations **/ wenzelm@65012: wenzelm@65740: /* statements */ wenzelm@65740: wenzelm@65740: class Statement private[SQL](val db: Database, val rep: PreparedStatement) wenzelm@69393: extends AutoCloseable wenzelm@65740: { wenzelm@65740: stmt => wenzelm@65740: wenzelm@65748: object bool wenzelm@65740: { wenzelm@65748: def update(i: Int, x: Boolean) { rep.setBoolean(i, x) } wenzelm@65748: def update(i: Int, x: Option[Boolean]) wenzelm@65748: { wenzelm@65748: if (x.isDefined) update(i, x.get) wenzelm@65748: else rep.setNull(i, java.sql.Types.BOOLEAN) wenzelm@65748: } wenzelm@65740: } wenzelm@65748: object int wenzelm@65740: { wenzelm@65748: def update(i: Int, x: Int) { rep.setInt(i, x) } wenzelm@65748: def update(i: Int, x: Option[Int]) wenzelm@65748: { wenzelm@65748: if (x.isDefined) update(i, x.get) wenzelm@65748: else rep.setNull(i, java.sql.Types.INTEGER) wenzelm@65748: } wenzelm@65740: } wenzelm@65748: object long wenzelm@65740: { wenzelm@65748: def update(i: Int, x: Long) { rep.setLong(i, x) } wenzelm@65748: def update(i: Int, x: Option[Long]) wenzelm@65748: { wenzelm@65748: if (x.isDefined) update(i, x.get) wenzelm@65748: else rep.setNull(i, java.sql.Types.BIGINT) wenzelm@65748: } wenzelm@65740: } wenzelm@65748: object double wenzelm@65740: { wenzelm@65748: def update(i: Int, x: Double) { rep.setDouble(i, x) } wenzelm@65748: def update(i: Int, x: Option[Double]) wenzelm@65748: { wenzelm@65748: if (x.isDefined) update(i, x.get) wenzelm@65748: else rep.setNull(i, java.sql.Types.DOUBLE) wenzelm@65748: } wenzelm@65748: } wenzelm@65748: object string wenzelm@65748: { wenzelm@65748: def update(i: Int, x: String) { rep.setString(i, x) } wenzelm@65748: def update(i: Int, x: Option[String]): Unit = update(i, x.orNull) wenzelm@65740: } wenzelm@65748: object bytes wenzelm@65740: { wenzelm@65748: def update(i: Int, bytes: Bytes) wenzelm@65748: { wenzelm@65748: if (bytes == null) rep.setBytes(i, null) wenzelm@65748: else rep.setBinaryStream(i, bytes.stream(), bytes.length) wenzelm@65748: } wenzelm@65748: def update(i: Int, bytes: Option[Bytes]): Unit = update(i, bytes.orNull) wenzelm@65740: } wenzelm@65748: object date wenzelm@65748: { wenzelm@65748: def update(i: Int, date: Date): Unit = db.update_date(stmt, i, date) wenzelm@65748: def update(i: Int, date: Option[Date]): Unit = update(i, date.orNull) wenzelm@65748: } wenzelm@65740: wenzelm@65740: def execute(): Boolean = rep.execute() wenzelm@65740: def execute_query(): Result = new Result(this, rep.executeQuery()) wenzelm@65740: wenzelm@65740: def close(): Unit = rep.close wenzelm@65740: } wenzelm@65740: wenzelm@65740: wenzelm@63790: /* results */ wenzelm@63790: wenzelm@65740: class Result private[SQL](val stmt: Statement, val rep: ResultSet) wenzelm@63790: { wenzelm@65740: res => wenzelm@65740: wenzelm@65740: def next(): Boolean = rep.next() wenzelm@65740: wenzelm@65740: def iterator[A](get: Result => A): Iterator[A] = new Iterator[A] wenzelm@65740: { wenzelm@65740: private var _next: Boolean = res.next() wenzelm@65740: def hasNext: Boolean = _next wenzelm@65740: def next: A = { val x = get(res); _next = res.next(); x } wenzelm@65740: } wenzelm@65740: wenzelm@65740: def bool(column: Column): Boolean = rep.getBoolean(column.name) wenzelm@65740: def int(column: Column): Int = rep.getInt(column.name) wenzelm@65740: def long(column: Column): Long = rep.getLong(column.name) wenzelm@65740: def double(column: Column): Double = rep.getDouble(column.name) wenzelm@65740: def string(column: Column): String = wenzelm@65740: { wenzelm@65740: val s = rep.getString(column.name) wenzelm@65740: if (s == null) "" else s wenzelm@65740: } wenzelm@65740: def bytes(column: Column): Bytes = wenzelm@65740: { wenzelm@65740: val bs = rep.getBytes(column.name) wenzelm@65740: if (bs == null) Bytes.empty else Bytes(bs) wenzelm@65740: } wenzelm@65740: def date(column: Column): Date = stmt.db.date(res, column) wenzelm@65740: wenzelm@65741: def timing(c1: Column, c2: Column, c3: Column) = wenzelm@65741: Timing(Time.ms(long(c1)), Time.ms(long(c2)), Time.ms(long(c3))) wenzelm@65741: wenzelm@65740: def get[A](column: Column, f: Column => A): Option[A] = wenzelm@65740: { wenzelm@65740: val x = f(column) wenzelm@65740: if (rep.wasNull) None else Some(x) wenzelm@65740: } wenzelm@65740: def get_bool(column: Column): Option[Boolean] = get(column, bool _) wenzelm@65740: def get_int(column: Column): Option[Int] = get(column, int _) wenzelm@65740: def get_long(column: Column): Option[Long] = get(column, long _) wenzelm@65740: def get_double(column: Column): Option[Double] = get(column, double _) wenzelm@65740: def get_string(column: Column): Option[String] = get(column, string _) wenzelm@65740: def get_bytes(column: Column): Option[Bytes] = get(column, bytes _) wenzelm@65740: def get_date(column: Column): Option[Date] = get(column, date _) wenzelm@63790: } wenzelm@65006: wenzelm@65740: wenzelm@65740: /* database */ wenzelm@65740: wenzelm@69393: trait Database extends AutoCloseable wenzelm@65006: { wenzelm@65740: db => wenzelm@65740: wenzelm@65740: wenzelm@65008: /* types */ wenzelm@65008: wenzelm@65730: def sql_type(T: Type.Value): Source wenzelm@65008: wenzelm@65008: wenzelm@65006: /* connection */ wenzelm@65006: wenzelm@65006: def connection: Connection wenzelm@65006: wenzelm@65006: def close() { connection.close } wenzelm@65006: wenzelm@65006: def transaction[A](body: => A): A = wenzelm@65006: { wenzelm@65006: val auto_commit = connection.getAutoCommit wenzelm@65006: try { wenzelm@65006: connection.setAutoCommit(false) wenzelm@65022: val savepoint = connection.setSavepoint wenzelm@65022: try { wenzelm@65022: val result = body wenzelm@65022: connection.commit wenzelm@65022: result wenzelm@65022: } wenzelm@65022: catch { case exn: Throwable => connection.rollback(savepoint); throw exn } wenzelm@65006: } wenzelm@65006: finally { connection.setAutoCommit(auto_commit) } wenzelm@65006: } wenzelm@65006: wenzelm@65006: wenzelm@65740: /* statements and results */ wenzelm@65740: wenzelm@65740: def statement(sql: Source): Statement = wenzelm@65740: new Statement(db, connection.prepareStatement(sql)) wenzelm@65006: wenzelm@65740: def using_statement[A](sql: Source)(f: Statement => A): A = wenzelm@65740: using(statement(sql))(f) wenzelm@65006: wenzelm@65748: def update_date(stmt: Statement, i: Int, date: Date): Unit wenzelm@65740: def date(res: Result, column: Column): Date wenzelm@65006: wenzelm@65730: def insert_permissive(table: Table, sql: Source = ""): Source wenzelm@65703: wenzelm@65006: wenzelm@65669: /* tables and views */ wenzelm@65006: wenzelm@65006: def tables: List[String] = wenzelm@65740: { wenzelm@65740: val result = new mutable.ListBuffer[String] wenzelm@65740: val rs = connection.getMetaData.getTables(null, null, "%", null) wenzelm@65740: while (rs.next) { result += rs.getString(3) } wenzelm@65740: result.toList wenzelm@65740: } wenzelm@65006: wenzelm@65730: def create_table(table: Table, strict: Boolean = false, sql: Source = ""): Unit = wenzelm@65698: using_statement( wenzelm@65698: table.create(strict, sql_type) + (if (sql == "") "" else " " + sql))(_.execute()) wenzelm@65006: wenzelm@65018: def create_index(table: Table, name: String, columns: List[Column], wenzelm@65280: strict: Boolean = false, unique: Boolean = false): Unit = wenzelm@65698: using_statement(table.create_index(name, columns, strict, unique))(_.execute()) wenzelm@65006: wenzelm@65691: def create_view(table: Table, strict: Boolean = false): Unit = wenzelm@65691: { wenzelm@65691: if (strict || !tables.contains(table.name)) { wenzelm@65731: val sql = "CREATE VIEW " + table + " AS " + { table.query; table.body } wenzelm@65698: using_statement(sql)(_.execute()) wenzelm@65690: } wenzelm@65691: } wenzelm@65006: } wenzelm@63778: } wenzelm@65006: wenzelm@65006: wenzelm@65006: wenzelm@65006: /** SQLite **/ wenzelm@65006: wenzelm@65006: object SQLite wenzelm@65006: { wenzelm@65021: // see https://www.sqlite.org/lang_datefunc.html wenzelm@65021: val date_format: Date.Format = Date.Format("uuuu-MM-dd HH:mm:ss.SSS x") wenzelm@65021: wenzelm@65890: lazy val init_jdbc: Unit = wenzelm@65890: { wenzelm@65890: val lib_path = Path.explode("$ISABELLE_SQLITE_HOME/" + Platform.jvm_platform) wenzelm@65890: val lib_name = wenzelm@65890: File.find_files(lib_path.file) match { wenzelm@65890: case List(file) => file.getName wenzelm@65918: case _ => error("Exactly one file expected in directory " + lib_path.expand) wenzelm@65890: } wenzelm@65890: System.setProperty("org.sqlite.lib.path", File.platform_path(lib_path)) wenzelm@65890: System.setProperty("org.sqlite.lib.name", lib_name) wenzelm@65890: wenzelm@65890: Class.forName("org.sqlite.JDBC") wenzelm@65890: } wenzelm@65292: wenzelm@65006: def open_database(path: Path): Database = wenzelm@65006: { wenzelm@65292: init_jdbc wenzelm@65006: val path0 = path.expand wenzelm@65006: val s0 = File.platform_path(path0) wenzelm@65006: val s1 = if (Platform.is_windows) s0.replace('\\', '/') else s0 wenzelm@65006: val connection = DriverManager.getConnection("jdbc:sqlite:" + s1) wenzelm@65007: new Database(path0.toString, connection) wenzelm@65006: } wenzelm@65006: wenzelm@65007: class Database private[SQLite](name: String, val connection: Connection) extends SQL.Database wenzelm@65006: { wenzelm@65007: override def toString: String = name wenzelm@65006: wenzelm@65730: def sql_type(T: SQL.Type.Value): SQL.Source = SQL.sql_type_sqlite(T) wenzelm@65011: wenzelm@65748: def update_date(stmt: SQL.Statement, i: Int, date: Date): Unit = wenzelm@65748: if (date == null) stmt.string(i) = (null: String) wenzelm@65748: else stmt.string(i) = date_format(date) wenzelm@65598: wenzelm@65740: def date(res: SQL.Result, column: SQL.Column): Date = wenzelm@65740: date_format.parse(res.string(column)) wenzelm@65021: wenzelm@65730: def insert_permissive(table: SQL.Table, sql: SQL.Source = ""): SQL.Source = wenzelm@65703: table.insert_cmd("INSERT OR IGNORE", sql = sql) wenzelm@65703: wenzelm@65698: def rebuild { using_statement("VACUUM")(_.execute()) } wenzelm@65006: } wenzelm@65006: } wenzelm@65006: wenzelm@65006: wenzelm@65006: wenzelm@65006: /** PostgreSQL **/ wenzelm@65006: wenzelm@65006: object PostgreSQL wenzelm@65006: { wenzelm@65006: val default_port = 5432 wenzelm@65006: wenzelm@65292: lazy val init_jdbc: Unit = Class.forName("org.postgresql.Driver") wenzelm@65292: wenzelm@65006: def open_database( wenzelm@65006: user: String, wenzelm@65006: password: String, wenzelm@65006: database: String = "", wenzelm@65006: host: String = "", wenzelm@65594: port: Int = 0, wenzelm@65636: ssh: Option[SSH.Session] = None, wenzelm@65636: ssh_close: Boolean = false): Database = wenzelm@65006: { wenzelm@65292: init_jdbc wenzelm@65292: wenzelm@65641: if (user == "") error("Undefined database user") wenzelm@65009: wenzelm@65717: val db_host = proper_string(host) getOrElse "localhost" wenzelm@65594: val db_port = if (port > 0 && port != default_port) ":" + port else "" wenzelm@65717: val db_name = "/" + (proper_string(database) getOrElse user) wenzelm@65009: wenzelm@65010: val (url, name, port_forwarding) = wenzelm@65009: ssh match { wenzelm@65010: case None => wenzelm@65010: val spec = db_host + db_port + db_name wenzelm@65010: val url = "jdbc:postgresql://" + spec wenzelm@65010: val name = user + "@" + spec wenzelm@65010: (url, name, None) wenzelm@65009: case Some(ssh) => wenzelm@65594: val fw = wenzelm@65594: ssh.port_forwarding(remote_host = db_host, wenzelm@65636: remote_port = if (port > 0) port else default_port, wenzelm@65636: ssh_close = ssh_close) wenzelm@65010: val url = "jdbc:postgresql://localhost:" + fw.local_port + db_name wenzelm@65010: val name = user + "@" + fw + db_name + " via ssh " + ssh wenzelm@65010: (url, name, Some(fw)) wenzelm@65009: } wenzelm@65009: try { wenzelm@65010: val connection = DriverManager.getConnection(url, user, password) wenzelm@65010: new Database(name, connection, port_forwarding) wenzelm@65009: } wenzelm@65009: catch { case exn: Throwable => port_forwarding.foreach(_.close); throw exn } wenzelm@65006: } wenzelm@65006: wenzelm@65009: class Database private[PostgreSQL]( wenzelm@65009: name: String, val connection: Connection, port_forwarding: Option[SSH.Port_Forwarding]) wenzelm@65009: extends SQL.Database wenzelm@65006: { wenzelm@65010: override def toString: String = name wenzelm@65008: wenzelm@65730: def sql_type(T: SQL.Type.Value): SQL.Source = SQL.sql_type_postgresql(T) wenzelm@65009: wenzelm@65021: // see https://jdbc.postgresql.org/documentation/head/8-date-time.html wenzelm@65748: def update_date(stmt: SQL.Statement, i: Int, date: Date): Unit = wenzelm@65740: if (date == null) stmt.rep.setObject(i, null) wenzelm@69980: else stmt.rep.setObject(i, OffsetDateTime.from(date.to(Date.timezone_utc).rep)) wenzelm@65598: wenzelm@65740: def date(res: SQL.Result, column: SQL.Column): Date = wenzelm@65704: { wenzelm@65740: val obj = res.rep.getObject(column.name, classOf[OffsetDateTime]) wenzelm@65704: if (obj == null) null else Date.instant(obj.toInstant) wenzelm@65704: } wenzelm@65021: wenzelm@65730: def insert_permissive(table: SQL.Table, sql: SQL.Source = ""): SQL.Source = wenzelm@65703: table.insert_cmd("INSERT", wenzelm@65703: sql = sql + (if (sql == "") "" else " ") + "ON CONFLICT DO NOTHING") wenzelm@65703: wenzelm@65009: override def close() { super.close; port_forwarding.foreach(_.close) } wenzelm@65006: } wenzelm@65006: }