changeset 1392:002152af497a

hosted postgres
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 06 Sep 2019 00:19:47 -0600
parents 94f48cc76de8
children cc0dbca576dc
files examples/blog/push-local.sh examples/blog/src/lib/Db.luan src/luan/host/WebHandler.java src/luan/host/init.luan src/luan/modules/Utils.java src/luan/modules/logging/LuanLogger.java src/luan/modules/lucene/Lucene.luan src/luan/modules/lucene/LuceneIndex.java src/luan/modules/lucene/PostgresBackup.java src/luan/modules/sql/Database.java src/luan/modules/url/LuanUrl.java
diffstat 11 files changed, 229 insertions(+), 190 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/blog/push-local.sh	Fri Sep 06 00:19:47 2019 -0600
@@ -0,0 +1,1 @@
+luan luan:host/push.luan blog.me.luan.ws word src 2>&1 | tee err
--- a/examples/blog/src/lib/Db.luan	Thu Sep 05 01:29:57 2019 -0600
+++ b/examples/blog/src/lib/Db.luan	Fri Sep 06 00:19:47 2019 -0600
@@ -1,16 +1,23 @@
 local Lucene = require "luan:lucene/Lucene.luan"
 local Io = require "luan:Io.luan"
+local Hosting = require "luan:host/Hosting.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Db"
 
 
 local Db = {}
 
-local function completer(doc)
-	return doc
+local postgres_spec = Hosting.postgres_spec and Hosting.postgres_spec()
+logger.info("postgres_spec="..postgres_spec)
+if postgres_spec ~= nil then
+	function postgres_spec.completer(doc)
+		return doc
+	end
 end
 
 function Db.new(lucene_dir)
 	local dir = Io.uri(lucene_dir)
-	local db = Lucene.index( dir, Lucene.type.english, {"subject","content"}, completer )
+	local db = Lucene.index( dir, Lucene.type.english, {"subject","content"}, postgres_spec )
 	
 --	this is how you index a field
 --	db.indexed_fields.post_date = Lucene.type.long
--- a/src/luan/host/WebHandler.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/host/WebHandler.java	Fri Sep 06 00:19:47 2019 -0600
@@ -33,7 +33,7 @@
 
 			Luan luan = new Luan();
 			Log4j.newLoggerRepository(luan);
-			initLuan(luan,dirStr,domain,true);
+			initLuan(luan,dirStr,domain);
 			return new LuanHandler(luan);
 		}
 	};
@@ -79,11 +79,11 @@
 		return true;
 	}
 */
-	private static void initLuan(Luan luan,String dir,String domain,boolean logging) {
+	private static void initLuan(Luan luan,String dir,String domain) {
 		security(luan,dir);
 		try {
 			LuanFunction fn = BasicLuan.load_file(luan,"classpath:luan/host/init.luan");
-			fn.call(dir,domain,logging);
+			fn.call(dir,domain);
 		} catch(LuanException e) {
 			throw new LuanRuntimeException(e);
 		}
--- a/src/luan/host/init.luan	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/host/init.luan	Fri Sep 06 00:19:47 2019 -0600
@@ -5,12 +5,12 @@
 local gsub = String.gsub or error()
 
 
-local dir, domain, logging = ...
+local dir, domain = ...
 
 
 -- logging
 
-if logging then
+do
 	require "java"
 	local Log4j = require "java:luan.modules.logging.Log4j"
 	local Level = require "java:org.apache.log4j.Level"
@@ -37,7 +37,6 @@
 end
 
 
-
 -- set vars
 
 local Io = require "luan:Io.luan"
@@ -66,6 +65,44 @@
 }.send
 
 
+
+
+-- postgres
+
+local Sql = require "luan:sql/Sql.luan"
+local database = Sql.database or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "init"
+
+local fn = Luan.load_file("file:postgres.luan") or error()
+local pg = fn()
+
+function Hosting.postgres_spec()
+	logger.info("pg="..pg.." domain="..domain)
+	if pg == nil then
+		return nil
+	end
+	local spec = {
+		class = "org.postgresql.Driver"
+		url = "jdbc:postgresql://localhost:5432/"..domain
+		user = domain
+		password = Io.password
+	}
+	local db = database(pg)
+	local exists = db.query("select datname from pg_database where datname=?",domain)() ~= nil;
+	logger.info("exists "..exists)
+	if not exists then
+		db.update( [[create user "]]..spec.user..[[" with encrypted password ']]..spec.password..[[']] )
+		db.update( [[create database "]]..domain..[[" owner "]]..spec.user..[["]] )
+	end
+	db.close()
+	return spec
+end
+
+
+
+
+
 -- callback to luanhost code
 do_file "file:init.luan"
 
--- a/src/luan/modules/Utils.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/Utils.java	Fri Sep 06 00:19:47 2019 -0600
@@ -104,6 +104,13 @@
 		return (String)val;
 	}
 
+	public static String removeRequiredString(Map map,String key) throws LuanException {
+		String s = removeString(map,key);
+		if( s==null )
+			throw new LuanException( "parameter '"+key+"' is required" );
+		return s;
+	}
+
 	public static Number removeNumber(Map map,String key) throws LuanException {
 		Object val = map.remove(key);
 		if( val!=null && !(val instanceof Number) )
@@ -111,7 +118,7 @@
 		return (Number)val;
 	}
 
-	public static Integer removeInt(Map map,String key) throws LuanException {
+	public static Integer removeInteger(Map map,String key) throws LuanException {
 		Object val = map.remove(key);
 		if( val==null )
 			return null;
@@ -137,6 +144,20 @@
 		return (Boolean)val;
 	}
 
+	public static LuanFunction removeFunction(Map map,String key) throws LuanException {
+		Object val = map.remove(key);
+		if( val!=null && !(val instanceof LuanFunction) )
+			throw new LuanException( "parameter '"+key+"' must be a function but is a "+Luan.type(val) );
+		return (LuanFunction)val;
+	}
+
+	public static LuanFunction removeRequiredFunction(Map map,String key) throws LuanException {
+		LuanFunction fn = removeFunction(map,key);
+		if( fn==null )
+			throw new LuanException( "parameter '"+key+"' is required" );
+		return fn;
+	}
+
 	public static void checkEmpty(Map map) throws LuanException {
 		if( !map.isEmpty() )
 			throw new LuanException( "unrecognized options: "+map );
--- a/src/luan/modules/logging/LuanLogger.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/logging/LuanLogger.java	Fri Sep 06 00:19:47 2019 -0600
@@ -46,6 +46,15 @@
 		}
 	}
 
+	public static Logger getLogger(Luan luan,Class cls) {
+		tl.set(luan);
+		try {
+			return LoggerFactory.getLogger(cls);
+		} finally {
+			tl.remove();
+		}
+	}
+
 	public static Luan luan() {
 		return tl.get();
 	}
--- a/src/luan/modules/lucene/Lucene.luan	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/lucene/Lucene.luan	Fri Sep 06 00:19:47 2019 -0600
@@ -35,15 +35,16 @@
 
 Lucene.literal = SaneQueryParser.literal
 
-function Lucene.index(index_dir,default_type,default_fields,completer)
+function Lucene.index(index_dir,default_type,default_fields,postgres_spec)
 	type(index_dir)=="table" or error "index_dir must be table"
 	index_dir.to_uri_string and matches(index_dir.to_uri_string(),"^file:") or error "must be file"
+	postgres_spec==nil or type(postgres_spec)=="table" or error "postgres_spec must be table"
 	local index = {}
 	index.dir = index_dir
-	local java_index, closer = LuceneIndex.getLuceneIndex(index_dir.java.file,default_type,default_fields,completer)
+	local java_index, closer = LuceneIndex.getLuceneIndex(index_dir.java.file,default_type,default_fields,postgres_spec)
 	index.java = java_index
 	index.closer = closer or error()
-	index.completer = completer
+	index.completer = postgres_spec and postgres_spec.completer
 
 	index.indexed_fields = {}
 	local mt = {}
--- a/src/luan/modules/lucene/LuceneIndex.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/lucene/LuceneIndex.java	Fri Sep 06 00:19:47 2019 -0600
@@ -79,6 +79,7 @@
 import luan.LuanException;
 import luan.LuanRuntimeException;
 import luan.modules.parsers.LuanToString;
+import luan.modules.logging.LuanLogger;
 import luan.lib.logging.Logger;
 import luan.lib.logging.LoggerFactory;
 
@@ -98,7 +99,11 @@
 
 		public void close() throws IOException {
 			if( !isClosed ) {
-				li.close();
+				try {
+					li.close();
+				} catch(SQLException e) {
+					throw new RuntimeException(e);
+				}
 				isClosed = true;
 			}
 		}
@@ -114,14 +119,14 @@
 
 	private static Map<String,LuceneIndex> indexes = new HashMap<String,LuceneIndex>();
 
-	public static Object[] getLuceneIndex(Luan luan,File indexDir,FieldParser defaultFieldParser,String[] defaultFields,LuanFunction completer)
-		throws LuanException, IOException
+	public static Object[] getLuceneIndex(Luan luan,File indexDir,FieldParser defaultFieldParser,String[] defaultFields,LuanTable postgresSpec)
+		throws LuanException, IOException, ClassNotFoundException, SQLException
 	{
 		String key = indexDir.getCanonicalPath();
 		synchronized(indexes) {
 			LuceneIndex li = indexes.get(key);
 			if( li == null ) {
-				li = new LuceneIndex(indexDir,defaultFieldParser,defaultFields,key,completer);
+				li = new LuceneIndex(luan,indexDir,defaultFieldParser,defaultFields,key,postgresSpec);
 				li.openCount = 1;
 				indexes.put(key,li);
 			} else {
@@ -163,9 +168,10 @@
 
 	private final PostgresBackup postgresBackup;
 
-	private LuceneIndex(File indexDir,FieldParser defaultFieldParser,String[] defaultFields,String key,LuanFunction completer)
-		throws LuanException, IOException
+	private LuceneIndex(Luan luan,File indexDir,FieldParser defaultFieldParser,String[] defaultFields,String key,LuanTable postgresSpec)
+		throws LuanException, IOException, ClassNotFoundException, SQLException
 	{
+		final Logger logger = LuanLogger.getLogger(luan,LuceneIndex.class);
 		this.key = key;
 		this.defaultFieldParser = defaultFieldParser;
 		this.defaultFields = defaultFields;
@@ -180,14 +186,20 @@
 		}
 		this.analyzer = analyzer;
 		boolean wasCreated = reopen();
-		postgresBackup = completer!=null ? PostgresBackup.newInstance() : null;
-		if( postgresBackup != null ) {
-			if( !wasCreated && postgresBackup.wasCreated ) {
-				logger.error("rebuilding postgres backup");
-				rebuild_postgres_backup(completer);
-			} else if( wasCreated && !postgresBackup.wasCreated ) {
-				logger.error("restoring from postgres");
-				restore_from_postgres();
+		if( postgresSpec == null ) {
+			postgresBackup = null;
+		} else {
+			Map spec = postgresSpec.asMap();
+			LuanFunction completer = Utils.removeRequiredFunction(spec,"completer");
+			postgresBackup = new PostgresBackup(spec);
+			if( postgresBackup != null ) {
+				if( !wasCreated && postgresBackup.wasCreated ) {
+					logger.error("rebuilding postgres backup");
+					rebuild_postgres_backup(completer);
+				} else if( wasCreated && !postgresBackup.wasCreated ) {
+					logger.error("restoring from postgres");
+					restore_from_postgres(luan);
+				}
 			}
 		}
 	}
@@ -210,7 +222,7 @@
 		writeCounter.incrementAndGet();
 	}
 
-	public void delete_all() throws IOException {
+	public void delete_all() throws IOException, SQLException {
 		boolean commit = !writeLock.isHeldByCurrentThread();
 		writeLock.lock();
 		try {
@@ -232,7 +244,7 @@
 	}
 
 	private void backupDelete(Query query)
-		throws IOException
+		throws IOException, SQLException, LuanException
 	{
 		if( postgresBackup != null ) {
 			final List<Long> ids = new ArrayList<Long>();
@@ -258,7 +270,7 @@
 	}
 
 	public void delete(String queryStr)
-		throws IOException, ParseException
+		throws IOException, ParseException, SQLException, LuanException
 	{
 		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
 
@@ -279,10 +291,10 @@
 	}
 
 	public void save(LuanTable doc,LuanTable boosts)
-		throws LuanException, IOException
+		throws LuanException, IOException, SQLException
 	{
 		if( boosts!=null && postgresBackup!=null )
-			logger.error("boosts are not saved to postgres backup");
+			throw new LuanException("boosts are not saved to postgres backup");
 
 		Object obj = doc.get("id");
 		Long id;
@@ -313,7 +325,9 @@
 		}
 	}
 
-	public Object run_in_transaction(LuanFunction fn) throws IOException, LuanException {
+	public Object run_in_transaction(LuanFunction fn)
+		throws IOException, LuanException, SQLException
+	{
 		boolean commit = !writeLock.isHeldByCurrentThread();
 		writeLock.lock();
 		boolean ok = false;
@@ -433,7 +447,7 @@
 		return writer.getDirectory().toString();
 	}
 
-	private synchronized void close() throws IOException {
+	private synchronized void close() throws IOException, SQLException {
 		if( openCount > 0 ) {
 			if( --openCount == 0 ) {
 				doClose();
@@ -444,11 +458,11 @@
 		}
 	}
 
-	public void doClose() throws IOException {
+	public void doClose() throws IOException, SQLException {
+		writer.close();
+		reader.close();
 		if( postgresBackup != null )
 			postgresBackup.close();
-		writer.close();
-		reader.close();
 	}
 
 
@@ -806,8 +820,10 @@
 	}
 
 	public void rebuild_postgres_backup(LuanFunction completer)
-		throws IOException, LuanException
+		throws IOException, LuanException, SQLException
 	{
+		final Logger logger = LuanLogger.getLogger(completer.luan(),LuceneIndex.class);
+		logger.info("start rebuild_postgres_backup");
 		writeLock.lock();
 		IndexSearcher searcher = openSearcher();
 		boolean ok = false;
@@ -824,6 +840,8 @@
 						postgresBackup.add(tbl);
 					} catch(LuanException e) {
 						throw new LuanRuntimeException(e);
+					} catch(SQLException e) {
+						throw new RuntimeException(e);
 					}
 				}
 			};
@@ -840,11 +858,14 @@
 				postgresBackup.rollback();
 			writeLock.unlock();
 		}
+		logger.info("end rebuild_postgres_backup");
 	}
 
-	public void restore_from_postgres()
-		throws IOException, LuanException
+	public void restore_from_postgres(Luan luan)
+		throws IOException, LuanException, SQLException
 	{
+		final Logger logger = LuanLogger.getLogger(luan,LuceneIndex.class);
+		logger.warn("start restore_from_postgres");
 		if( postgresBackup==null )
 			throw new NullPointerException();
 		if( writeLock.isHeldByCurrentThread() )
@@ -866,6 +887,7 @@
 			wrote();
 			writeLock.unlock();
 		}
+		logger.warn("end restore_from_postgres");
 	}
 
 	void restore(LuanTable doc)
@@ -875,6 +897,7 @@
 	}
 
 	public void check(LuanFunction completer) throws IOException, SQLException, LuanException {
+		final Logger logger = LuanLogger.getLogger(completer.luan(),LuceneIndex.class);
 		logger.info("start check");
 		CheckIndex.Status status = new CheckIndex(fsDir).checkIndex();
 		if( !status.clean )
@@ -885,6 +908,7 @@
 	}
 
 	private void checkPostgres(LuanFunction completer) throws IOException, SQLException, LuanException {
+		final Logger logger = LuanLogger.getLogger(completer.luan(),LuceneIndex.class);
 		final PostgresBackup.Checker postgresChecker;
 		final IndexSearcher searcher;
 		writeLock.lock();
--- a/src/luan/modules/lucene/PostgresBackup.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/lucene/PostgresBackup.java	Fri Sep 06 00:19:47 2019 -0600
@@ -10,11 +10,14 @@
 import java.util.Properties;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Map;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanFunction;
 import luan.LuanException;
+import luan.modules.Utils;
 import luan.modules.parsers.LuanToString;
+import luan.modules.logging.LuanLogger;
 import luan.lib.logging.Logger;
 import luan.lib.logging.LoggerFactory;
 
@@ -22,18 +25,6 @@
 final class PostgresBackup {
 	private static final Logger logger = LoggerFactory.getLogger(PostgresBackup.class);
 
-	static PostgresBackup newInstance() {
-		try {
-			return new PostgresBackup();
-		} catch(ClassNotFoundException e) {
-			logger.error("creation failed",e);
-			return null;
-		} catch(SQLException e) {
-			logger.error("creation failed",e);
-			return null;
-		}
-	}
-
 	final boolean wasCreated;
 	private final String url;
 	private final Properties props = new Properties();
@@ -44,14 +35,23 @@
 	private int trans = 0;
 	private final LuanToString luanToString = new LuanToString();
 
-	private PostgresBackup()
-		throws ClassNotFoundException, SQLException
+	PostgresBackup(Map spec)
+		throws ClassNotFoundException, SQLException, LuanException
 	{
+/*
 		Class.forName("org.postgresql.Driver");
-
 		url = "jdbc:postgresql://localhost:5432/luan";
 		props.setProperty("user","postgres");
 		props.setProperty("password","");
+*/
+		String cls = "org.postgresql.Driver";
+		if( !Utils.removeRequiredString(spec,"class").equals(cls) )
+			throw new LuanException( "parameter 'class' must be '"+cls+"'" );
+		Class.forName(cls);
+		url = Utils.removeRequiredString(spec,"url");
+		props.setProperty( "user", Utils.removeRequiredString(spec,"user") );
+		props.setProperty( "password", Utils.removeRequiredString(spec,"password") );
+		Utils.checkEmpty(spec);
 
 		con = newConnection();
 
@@ -88,15 +88,11 @@
 		return DriverManager.getConnection(url,props);
 	}
 
-	void close() {
-		try {
-			insertStmt.close();
-			updateStmt.close();
-			deleteStmt.close();
-			con.close();
-		} catch(SQLException e) {
-			logger.error("close failed",e);
-		}
+	void close() throws SQLException {
+		insertStmt.close();
+		updateStmt.close();
+		deleteStmt.close();
+		con.close();
 	}
 
 	protected void finalize() throws Throwable {
@@ -107,90 +103,58 @@
 		}
 	}
 
-	void add(LuanTable doc) throws LuanException {
-		try {
-			Long id = (Long)doc.get("id");
-			String data = luanToString.toString(doc);
-			insertStmt.setLong(1,id);
-			insertStmt.setString(2,data);
-			insertStmt.executeUpdate();
-		} catch(SQLException e) {
-			logger.error("add failed",e);
-		}
+	void add(LuanTable doc) throws LuanException, SQLException {
+		Long id = (Long)doc.get("id");
+		String data = luanToString.toString(doc);
+		insertStmt.setLong(1,id);
+		insertStmt.setString(2,data);
+		insertStmt.executeUpdate();
 	}
 
-	void update(LuanTable doc) throws LuanException {
-		try {
-			Long id = (Long)doc.get("id");
-			String data = luanToString.toString(doc);
-			updateStmt.setString(1,data);
-			updateStmt.setLong(2,id);
-			int n = updateStmt.executeUpdate();
-			if( n==0 ) {
-				logger.error("update not found for id="+id+", trying add");
-				add(doc);
-			} else if( n!=1 )
-				throw new RuntimeException();
-		} catch(SQLException e) {
-			logger.error("update failed",e);
-		}
-	}
-
-	void deleteAll() {
-		try {
-			Statement stmt = con.createStatement();
-			stmt.executeUpdate("delete from lucene");
-			stmt.close();
-		} catch(SQLException e) {
-			logger.error("update failed",e);
-		}
+	void update(LuanTable doc) throws LuanException, SQLException {
+		Long id = (Long)doc.get("id");
+		String data = luanToString.toString(doc);
+		updateStmt.setString(1,data);
+		updateStmt.setLong(2,id);
+		int n = updateStmt.executeUpdate();
+		if( n==0 ) {
+			Logger logger = LuanLogger.getLogger(doc.luan(),PostgresBackup.class);
+			logger.error("update not found for id="+id+", trying add");
+			add(doc);
+		} else if( n!=1 )
+			throw new RuntimeException();
 	}
 
-	void delete(long id) {
-		try {
-			deleteStmt.setLong(1,id);
-			int n = deleteStmt.executeUpdate();
-			if( n==0 ) {
-				logger.error("delete not found for id="+id);
-			}
-		} catch(SQLException e) {
-			logger.error("update failed",e);
-		}
+	void deleteAll() throws SQLException {
+		Statement stmt = con.createStatement();
+		stmt.executeUpdate("delete from lucene");
+		stmt.close();
 	}
 
-	void begin() {
-		try {
-			if( trans++ == 0 )
-				con.setAutoCommit(false);
-		} catch(SQLException e) {
-			logger.error("begin failed",e);
-		}
+	void delete(long id) throws SQLException, LuanException {
+		deleteStmt.setLong(1,id);
+		int n = deleteStmt.executeUpdate();
+		if( n==0 )
+			throw new LuanException("delete not found for id="+id);
 	}
 
-	void commit() {
-		try {
-			if( trans <= 0 ) {
-				logger.error("commit not in transaction");
-				return;
-			}
-			if( --trans == 0 )
-				con.setAutoCommit(true);
-		} catch(SQLException e) {
-			logger.error("begin failed",e);
-		}
+	void begin() throws SQLException {
+		if( trans++ == 0 )
+			con.setAutoCommit(false);
 	}
 
-	void rollback() {
-		try {
-			if( --trans != 0 ) {
-				logger.error("rollback failed trans="+trans);
-				return;
-			}
-			con.rollback();
+	void commit() throws SQLException, LuanException {
+		if( trans <= 0 )
+			throw new LuanException("commit not in transaction");
+		if( --trans == 0 )
 			con.setAutoCommit(true);
-		} catch(SQLException e) {
-			logger.error("begin failed",e);
-		}
+	}
+
+	void rollback() throws SQLException, LuanException {
+		if( --trans != 0 )
+			throw new LuanException("rollback failed trans="+trans);
+		con.rollback();
+		con.setAutoCommit(true);
 	}
 
 	private static LuanTable newEnv() {
@@ -205,38 +169,28 @@
 	}
 
 	void restoreLucene(LuceneIndex li)
-		throws LuanException, IOException
+		throws LuanException, IOException, SQLException
 	{
-		try {
-			LuanTable env = newEnv();
-			Statement stmt = con.createStatement();
-			ResultSet rs = stmt.executeQuery("select data from lucene");
-			while( rs.next() ) {
-				String data = rs.getString("data");
-				LuanTable doc = (LuanTable)eval(data,env);
-				li.restore(doc);
-			}
-			stmt.close();
-		} catch(SQLException e) {
-			logger.error("restoreLucene failed",e);
-			throw new RuntimeException(e);
+		LuanTable env = newEnv();
+		Statement stmt = con.createStatement();
+		ResultSet rs = stmt.executeQuery("select data from lucene");
+		while( rs.next() ) {
+			String data = rs.getString("data");
+			LuanTable doc = (LuanTable)eval(data,env);
+			li.restore(doc);
 		}
+		stmt.close();
 	}
 
 	long maxId()
-		throws LuanException, IOException
+		throws LuanException, IOException, SQLException
 	{
-		try {
-			Statement stmt = con.createStatement();
-			ResultSet rs = stmt.executeQuery("select max(id) as m from lucene");
-			rs.next();
-			long m = rs.getLong("m");
-			stmt.close();
-			return m;
-		} catch(SQLException e) {
-			logger.error("maxId failed",e);
-			throw new RuntimeException(e);
-		}
+		Statement stmt = con.createStatement();
+		ResultSet rs = stmt.executeQuery("select max(id) as m from lucene");
+		rs.next();
+		long m = rs.getLong("m");
+		stmt.close();
+		return m;
 	}
 
 	final class Checker {
--- a/src/luan/modules/sql/Database.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/sql/Database.java	Fri Sep 06 00:19:47 2019 -0600
@@ -14,6 +14,7 @@
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanException;
+import luan.modules.Utils;
 
 
 public final class Database {
@@ -31,12 +32,14 @@
 		throws LuanException, ClassNotFoundException, SQLException
 	{
 		Map<Object,Object> spec = specTbl.asMap();
-		String cls = getString(spec,"class");
+		String cls = Utils.removeRequiredString(spec,"class");
 		Class.forName(cls);
-		String url = getString(spec,"url");
+		String url = Utils.removeRequiredString(spec,"url");
 		Properties props = new Properties();
 		props.putAll(spec);
 		this.con = DriverManager.getConnection(url,props);
+		spec.remove("user");
+		spec.remove("password");
 		set(spec);
 	}
 
@@ -45,31 +48,13 @@
 	}
 
 	private void set(Map<Object,Object> options) throws LuanException, SQLException {
-		Object obj;
-		obj = options.remove("auto_commit");
-		if( obj != null ) {
-			if( !(obj instanceof Boolean) )
-				throw new LuanException( "parameter 'auto_commit' must be a boolean" );
-			con.setAutoCommit((Boolean)obj);
-		}
-		obj = options.remove("fetch_size");
-		if( obj != null ) {
-			Integer n = Luan.asInteger(obj);
-			if( n == null )
-				throw new LuanException( "parameter 'fetch_size' must be an integer" );
+		Boolean autoCommit = Utils.removeBoolean(options,"auto_commit");
+		if( autoCommit != null )
+			con.setAutoCommit(autoCommit);
+		Integer n = Utils.removeInteger(options,"fetch_size");
+		if( n != null )
 			fetchSize = n;
-		}
-		if( !options.isEmpty() )
-			throw new LuanException( "unrecognized parameters: "+options );
-	}
-
-	private static String getString(Map spec,String key) throws LuanException {
-		Object val = spec.remove(key);
-		if( val==null )
-			throw new LuanException( "parameter '"+key+"' is required" );
-		if( !(val instanceof String) )
-			throw new LuanException( "parameter '"+key+"' must be a string" );
-		return (String)val;
+		Utils.checkEmpty(options);
 	}
 
 	private void fix(Statement stmt) throws SQLException {
--- a/src/luan/modules/url/LuanUrl.java	Thu Sep 05 01:29:57 2019 -0600
+++ b/src/luan/modules/url/LuanUrl.java	Fri Sep 06 00:19:47 2019 -0600
@@ -159,7 +159,7 @@
 					}
 				}
 			}
-			Integer timeout = Utils.removeInt(map,"time_out");
+			Integer timeout = Utils.removeInteger(map,"time_out");
 			if( timeout != null )
 				this.timeout = timeout;
 			Utils.checkEmpty(map);