提交 9a25c1c1 authored 作者: Thomas Mueller's avatar Thomas Mueller

--no commit message

--no commit message
上级 4f59a3c8
......@@ -28,6 +28,7 @@ import org.h2.value.ValueArray;
import org.h2.value.ValueBoolean;
import org.h2.value.ValueDouble;
import org.h2.value.ValueInt;
import org.h2.value.ValueLong;
import org.h2.value.ValueNull;
import org.h2.value.ValueString;
......@@ -151,7 +152,7 @@ public class Aggregate extends Expression {
switch(type) {
case COUNT_ALL:
Table table = select.getTopTableFilter().getTable();
return ValueInt.get(table.getRowCount());
return ValueLong.get(table.getRowCount());
case MIN:
case MAX:
boolean first = type == MIN;
......@@ -261,6 +262,10 @@ public class Aggregate extends Expression {
break;
case COUNT_ALL:
case COUNT:
dataType = Value.LONG;
scale = 0;
precision = 0;
break;
case SELECTIVITY:
dataType = Value.INT;
scale = 0;
......
......@@ -15,11 +15,12 @@ import org.h2.value.Value;
import org.h2.value.ValueBoolean;
import org.h2.value.ValueDouble;
import org.h2.value.ValueInt;
import org.h2.value.ValueLong;
import org.h2.value.ValueNull;
public class AggregateData {
private int aggregateType;
private int count;
private long count;
private ValueHashMap distinctValues;
private Value value;
private double sum, vpn;
......@@ -149,7 +150,7 @@ public class AggregateData {
}
case Aggregate.COUNT:
case Aggregate.COUNT_ALL:
v = ValueInt.get(count);
v = ValueLong.get(count);
break;
case Aggregate.SUM:
case Aggregate.MIN:
......@@ -199,12 +200,12 @@ public class AggregateData {
return v == null ? ValueNull.INSTANCE : v;
}
private Value divide(Value a, int count) throws SQLException {
private Value divide(Value a, long count) throws SQLException {
if(count == 0) {
return ValueNull.INSTANCE;
}
int type = Value.getHigherOrder(a.getType(), Value.INT);
Value b = ValueInt.get(count).convertTo(type);
int type = Value.getHigherOrder(a.getType(), Value.LONG);
Value b = ValueLong.get(count).convertTo(type);
a = a.convertTo(type).divide(b);
return a;
}
......
......@@ -136,7 +136,7 @@ public class CompareLike extends Condition {
return;
}
int dataType = l.getColumn().getType();
if(dataType != Value.STRING && dataType != Value.STRING_IGNORECASE) {
if(dataType != Value.STRING && dataType != Value.STRING_IGNORECASE && dataType != Value.STRING_FIXED) {
// column is not a varchar - can't use the index
return;
}
......
......@@ -45,7 +45,7 @@ public class ConditionExists extends Condition {
public String getSQL() {
StringBuffer buff = new StringBuffer();
buff.append("EXISTS(");
buff.append(query.getPlan());
buff.append(query.getPlanSQL());
buff.append(")");
return buff.toString();
}
......
......@@ -83,10 +83,10 @@ public class ConditionInSelect extends Condition {
if(left == ValueExpression.NULL) {
return left;
}
query.prepare();
if(query.getColumnCount() != 1) {
throw Message.getSQLException(Message.SUBQUERY_IS_NOT_SINGLE_COLUMN);
}
query.prepare();
}
// Can not optimize IN(SELECT...): the data may change
// However, could transform to an inner join
return this;
......@@ -101,7 +101,7 @@ public class ConditionInSelect extends Condition {
StringBuffer buff = new StringBuffer("(");
buff.append(left.getSQL());
buff.append(" IN(");
buff.append(query.getPlan());
buff.append(query.getPlanSQL());
buff.append("))");
return buff.toString();
}
......
......@@ -833,7 +833,7 @@ public class Function extends Expression implements FunctionCall {
}
}
Sequence getSequence(Session session, Value v0, Value v1) throws SQLException {
private Sequence getSequence(Session session, Value v0, Value v1) throws SQLException {
String schemaName, sequenceName;
if(v1 == null) {
schemaName = session.getCurrentSchemaName();
......
......@@ -60,7 +60,7 @@ public class Parameter extends Expression implements ParameterInterface {
public void checkSet() throws SQLException {
if (value == null) {
throw Message.getSQLException(Message.PARAMETER_NOT_SET_1, String.valueOf(index + 1));
throw Message.getSQLException(Message.PARAMETER_NOT_SET_1, "#" + (index + 1));
}
}
......
......@@ -28,7 +28,7 @@ public class ParameterRemote implements ParameterInterface {
public void checkSet() throws SQLException {
if (value == null) {
throw Message.getSQLException(Message.PARAMETER_NOT_SET_1, String.valueOf(index + 1));
throw Message.getSQLException(Message.PARAMETER_NOT_SET_1, "#" + (index + 1));
}
}
......
......@@ -80,7 +80,7 @@ public class Subquery extends Expression {
}
public String getSQL() {
return "(" + query.getPlan() +")";
return "(" + query.getPlanSQL() +")";
}
public void updateAggregate(Session session) throws SQLException {
......
......@@ -18,6 +18,7 @@ import org.h2.store.RecordReader;
import org.h2.store.Storage;
import org.h2.table.Column;
import org.h2.table.TableData;
import org.h2.util.MathUtils;
import org.h2.util.ObjectArray;
import org.h2.value.Value;
import org.h2.value.ValueNull;
......@@ -198,7 +199,7 @@ public class BtreeIndex extends Index implements RecordReader {
}
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
return 10 * getCostRangeIndex(masks, tableData.getRowCount());
}
......
......@@ -46,11 +46,11 @@ public class FunctionIndex extends Index {
return new FunctionCursor(result);
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
if(masks != null) {
throw Message.getUnsupportedException();
}
return Integer.MAX_VALUE;
return Long.MAX_VALUE;
}
public void remove(Session session) throws SQLException {
......
......@@ -120,13 +120,13 @@ public class HashIndex extends Index {
return new HashCursor(result);
}
public int getCost(int[] masks) {
public long getCost(int[] masks) {
for (int i = 0; i < columns.length; i++) {
Column column = columns[i];
int index = column.getColumnId();
int mask = masks[index];
if ((mask & IndexCondition.EQUALITY) != IndexCondition.EQUALITY) {
return Integer.MAX_VALUE;
return Long.MAX_VALUE;
}
}
return 2;
......
......@@ -30,7 +30,7 @@ public abstract class Index extends SchemaObject {
protected Table table;
public IndexType indexType;
public static final int EMPTY_HEAD = -1;
protected int rowCount;
protected long rowCount;
public Index(Table table, int id, String name, Column[] columns, IndexType indexType) {
super(table.getSchema(), id, name, Trace.INDEX);
......@@ -74,24 +74,24 @@ public abstract class Index extends SchemaObject {
public abstract void add(Session session, Row row) throws SQLException;
public abstract void remove(Session session, Row row) throws SQLException;
public abstract Cursor find(Session session, SearchRow first, SearchRow last) throws SQLException;
public abstract int getCost(int[] masks) throws SQLException;
public abstract long getCost(int[] masks) throws SQLException;
public abstract void remove(Session session) throws SQLException;
public abstract void truncate(Session session) throws SQLException;
public abstract boolean canGetFirstOrLast(boolean first);
public abstract Value findFirstOrLast(Session session, boolean first) throws SQLException;
public abstract boolean needRebuild();
public int getRowCount() {
public long getRowCount() {
return rowCount;
}
public int getLookupCost(int rowCount) {
public int getLookupCost(long rowCount) {
return 2;
}
public int getCostRangeIndex(int[] masks, int rowCount) throws SQLException {
public long getCostRangeIndex(int[] masks, long rowCount) throws SQLException {
rowCount += Constants.COST_ROW_OFFSET;
int cost = rowCount;
long cost = rowCount;
int totalSelectivity = 0;
for (int i = 0; masks != null && i < columns.length; i++) {
Column column = columns[i];
......@@ -103,11 +103,11 @@ public abstract class Index extends SchemaObject {
break;
}
totalSelectivity = 100 - ((100-totalSelectivity) * (100-column.getSelectivity()) / 100);
int distinctRows = rowCount * totalSelectivity / 100;
long distinctRows = rowCount * totalSelectivity / 100;
if(distinctRows <= 0) {
distinctRows = 1;
}
int rowsSelected = rowCount / distinctRows;
long rowsSelected = rowCount / distinctRows;
if(rowsSelected < 1) {
rowsSelected = 1;
}
......
......@@ -50,7 +50,7 @@ public class LinearHashIndex extends Index implements RecordReader {
this.tableData = table;
// TODO linear hash: currently, changes are not logged
String name = database.getName()+"."+id+Constants.SUFFIX_HASH_FILE;
diskFile = new DiskFile(database, name, false, false, Constants.DEFAULT_CACHE_SIZE_LINEAR_INDEX);
diskFile = new DiskFile(database, name, "rw", false, false, Constants.DEFAULT_CACHE_SIZE_LINEAR_INDEX);
diskFile.init();
bucketSize = 4 * DiskFile.BLOCK_SIZE - diskFile.getRecordOverhead();
blocksPerBucket = 4;
......@@ -487,13 +487,13 @@ public class LinearHashIndex extends Index implements RecordReader {
return new LinearHashCursor(tableData.getRow(key));
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
for (int i = 0; i < columns.length; i++) {
Column column = columns[i];
int index = column.getColumnId();
int mask = masks[index];
if ((mask & IndexCondition.EQUALITY) != IndexCondition.EQUALITY) {
return Integer.MAX_VALUE;
return Long.MAX_VALUE;
}
}
return 100;
......
......@@ -138,7 +138,7 @@ public class LinkedIndex extends Index {
}
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
return 100 + getCostRangeIndex(masks, rowCount + Constants.COST_ROW_OFFSET);
}
......
......@@ -48,7 +48,7 @@ public class MetaIndex extends Index {
return new MetaCursor(rows);
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
if(scan) {
return 10000;
}
......
......@@ -42,7 +42,7 @@ public class RangeIndex extends Index {
return new RangeCursor(start, end);
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
return 1;
}
......
......@@ -142,8 +142,8 @@ public class ScanIndex extends Index {
return new ScanCursor(this);
}
public int getCost(int[] masks) throws SQLException {
int cost = tableData.getRowCount() + Constants.COST_ROW_OFFSET;
public long getCost(int[] masks) throws SQLException {
long cost = tableData.getRowCount() + Constants.COST_ROW_OFFSET;
if(storage != null) {
cost *= 10;
}
......
......@@ -293,7 +293,7 @@ public class TreeIndex extends Index {
}
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
return getCostRangeIndex(masks, tableData.getRowCount());
}
......
......@@ -30,7 +30,7 @@ public class ViewIndex extends Index {
private ObjectArray originalParameters;
private Parameter[] params;
private SmallLRUCache costCache = new SmallLRUCache(Constants.VIEW_COST_CACHE_SIZE);
private SmallLRUCache costCache = new SmallLRUCache(Constants.VIEW_INDEX_CACHE_SIZE);
private Value[] lastParameters;
private long lastEvaluated;
......@@ -39,6 +39,8 @@ public class ViewIndex extends Index {
private int recurseLevel;
private LocalResult recursiveResult;
private String planSQL;
public ViewIndex(TableView view, String querySQL, ObjectArray originalParameters, boolean recursive) {
super(view, 0, null, null, IndexType.createNonUnique(false));
this.querySQL = querySQL;
......@@ -48,8 +50,24 @@ public class ViewIndex extends Index {
params = new Parameter[0];
}
public ViewIndex(TableView view, ViewIndex index, Session session, int[] masks) throws SQLException {
super(view, 0, null, null, IndexType.createNonUnique(false));
this.querySQL = index.querySQL;
this.originalParameters = index.originalParameters;
this.recursive = index.recursive;
columns = new Column[0];
params = new Parameter[0];
planSQL = getQuerySQL(session, masks);
}
public String getPlanSQL() {
return querySQL;
int testing;
// return sessionQuery.getPlanSQL();
// Query query = (Query)session.prepare(querySQL, true);
// return query.getPlanSQL();
return planSQL;
}
public void close(Session session) throws SQLException {
......@@ -82,10 +100,38 @@ public class ViewIndex extends Index {
double cost;
}
public double getCost(Session session, int[] masks) throws SQLException {
if(recursive) {
return 10;
private String getQuerySQL(Session session, int[] masks) throws SQLException {
if(masks == null) {
return querySQL;
}
Query query = (Query)session.prepare(querySQL, true);
IntArray paramIndex = new IntArray();
for(int i=0; i<masks.length; i++) {
int mask = masks[i];
if(mask == 0) {
continue;
}
paramIndex.add(i);
}
int len = paramIndex.size();
columns = new Column[len];
params = new Parameter[len];
for(int i=0; i<len; i++) {
int idx = paramIndex.get(i);
Column col = table.getColumn(idx);
columns[i] = col;
Parameter param = new Parameter(i);
params[i] = param;
int mask = masks[idx];
int comparisonType = getComparisonType(mask);
query.addGlobalCondition(param, idx, comparisonType);
}
String sql = query.getPlanSQL();
query = (Query)session.prepare(sql, true);
return query.getPlanSQL();
}
public double getCost(Session session, int[] masks) throws SQLException {
IntArray masksArray = new IntArray(masks == null ? new int[0] : masks);
CostElement cachedCost = (CostElement) costCache.get(masksArray);
if(cachedCost != null) {
......@@ -123,7 +169,11 @@ public class ViewIndex extends Index {
if(recursive) {
return 10;
}
String sql = query.getSQL();
int testing;
// String sql = query.getSQL();
String sql = query.getPlanSQL();
query = (Query)session.prepare(sql);
}
double cost = query.getCost();
......@@ -198,8 +248,15 @@ public class ViewIndex extends Index {
}
}
}
query.setSession(session);
LocalResult result = query.query(0);
String sql = query.getPlanSQL();
Query q2 = (Query)session.prepare(sql);
LocalResult result = q2.query(0);
int testing2;
// query.setSession(session);
// LocalResult result = query.query(0);
if(canReuse) {
lastResult = result;
lastParameters = params;
......@@ -208,11 +265,11 @@ public class ViewIndex extends Index {
return new ViewCursor(table, result);
}
public int getCost(int[] masks) throws SQLException {
public long getCost(int[] masks) throws SQLException {
if(masks != null) {
throw Message.getUnsupportedException();
}
return Integer.MAX_VALUE;
return Long.MAX_VALUE;
}
public void remove(Session session) throws SQLException {
......
......@@ -15,9 +15,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
//#ifdef JDK14
import java.sql.Savepoint;
//#endif
import java.sql.Statement;
import java.util.Map;
import java.util.Properties;
......@@ -239,25 +237,28 @@ public class JdbcConnection extends TraceObject implements Connection {
if(executingStatement != null) {
executingStatement.cancel();
}
if (session != null && !session.isClosed()) {
try {
rollbackInternal();
commit = closeAndSetNull(commit);
rollback = closeAndSetNull(rollback);
setAutoCommitTrue = closeAndSetNull(setAutoCommitTrue);
setAutoCommitFalse = closeAndSetNull(setAutoCommitFalse);
getAutoCommit = closeAndSetNull(getAutoCommit);
getReadOnly = closeAndSetNull(getReadOnly);
getGeneratedKeys = closeAndSetNull(getGeneratedKeys);
getLockMode = closeAndSetNull(getLockMode);
setLockMode = closeAndSetNull(setLockMode);
} finally {
if (session == null) {
return;
}
try {
if (!session.isClosed()) {
try {
session.close();
rollbackInternal();
commit = closeAndSetNull(commit);
rollback = closeAndSetNull(rollback);
setAutoCommitTrue = closeAndSetNull(setAutoCommitTrue);
setAutoCommitFalse = closeAndSetNull(setAutoCommitFalse);
getAutoCommit = closeAndSetNull(getAutoCommit);
getReadOnly = closeAndSetNull(getReadOnly);
getGeneratedKeys = closeAndSetNull(getGeneratedKeys);
getLockMode = closeAndSetNull(getLockMode);
setLockMode = closeAndSetNull(setLockMode);
} finally {
session = null;
session.close();
}
}
} finally {
session = null;
}
} catch(Throwable e) {
throw logAndConvert(e);
......
......@@ -242,7 +242,7 @@ public class Message {
public static final int IO_EXCEPTION_1 = 90028;
public static final int NOT_ON_UPDATABLE_ROW = 90029;
public static final int FILE_CORRUPTED_1 = 90030;
public static final int CONNECTION_NOT_CLOSED = 90031;
public static final int USER_NOT_FOUND_1 = 90032;
public static final int USER_ALREADY_EXISTS_1 = 90033;
public static final int LOG_FILE_ERROR_1 = 90034;
......
......@@ -68,14 +68,14 @@ public class TraceObject {
if(!trace.debug()) {
return;
}
trace.debugCode(className + " " + toString() + " = ");
trace.debugCode(className + " " + PREFIX[type] + id + " = ");
}
protected void infoCodeAssign(String className, int type, int id) {
if(!trace.info()) {
return;
}
trace.infoCode(className + " " + toString() + " = ");
trace.infoCode(className + " " + PREFIX[type] + id + " = ");
}
protected void debugCodeCall(String text) {
......
......@@ -11,10 +11,10 @@ import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import org.h2.engine.Constants;
import org.h2.util.FileUtils;
import org.h2.util.SmallLRUCache;
/**
* It is possible to write after close was called, but that means for each write the
......@@ -38,7 +38,7 @@ public class TraceSystem {
private int maxFileSize = DEFAULT_MAX_FILE_SIZE;
private String fileName;
private long lastCheck;
private HashMap traces;
private SmallLRUCache traces;
private SimpleDateFormat dateFormat;
private FileWriter fileWriter;
private PrintWriter printWriter;
......@@ -60,7 +60,7 @@ public class TraceSystem {
public TraceSystem(String fileName) {
this.fileName = fileName;
traces = new HashMap();
traces = new SmallLRUCache(100);
dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss ");
if(fileName != null) {
try {
......
......@@ -1087,14 +1087,18 @@ A natural join is an inner join, where the condition is automatically on the col
","
TEST AS T LEFT JOIN TEST AS T1 ON T.ID = T1.ID
"
"Other Grammar","Order","
{int | expression} [ASC | DESC]
[NULLS {FIRST | LAST}]
","
Groups the result by the column or expression.
Sorts the result by the given column number, or by an expression.
If the expression is a single parameter, then the value is interpreted
as a column number. Negative column numbers reverse the sort order.
","
NAME DESC NULLS LAST
"
"Other Grammar","Expression","
andCondition [OR andCondition]
","
......@@ -1218,7 +1222,7 @@ ID AS VALUE
"Other Grammar","Data Type","
intType | booleanType | tinyintType | smallintType | bigintType | identityType |
decimalType | doubleType | realType | dateType | timeType | timestampType |
binaryType | otherType | varcharType | varcharIgnorecaseType |
binaryType | otherType | varcharType | varcharIgnorecaseType | charType
blobType | clobType | uuidType | arrayType
","
A data type definition.
......@@ -1484,8 +1488,8 @@ OTHER
"
"Data Types","VARCHAR Type","
{VARCHAR | CHAR | CHARACTER | LONGVARCHAR |
VARCHAR2 | NCHAR | NVARCHAR | NVARCHAR2 | VARCHAR_CASESENSITIVE}
{VARCHAR | LONGVARCHAR |
VARCHAR2 | NVARCHAR | NVARCHAR2 | VARCHAR_CASESENSITIVE}
[( precisionInt )]
","
Unicode String. Use two single quotes ('') to create a quote.
......@@ -1505,6 +1509,19 @@ For large text data CLOB should be used.
VARCHAR_IGNORECASE
"
"Data Types","CHAR Type","
{CHAR | CHARACTER | NCHAR}
[( precisionInt )]
","
This type is supported for compatibility with other databases and older applications.
The difference to VARCHAR is that trailing spaces are ignored.
Unicode String. Use two single quotes ('') to create a quote.
There is no maximum precision. The maximum size is the memory available.
For large text data CLOB should be used.
","
CHAR(10)
"
"Data Types","BLOB Type","
{BLOB | TINYBLOB | MEDIUMBLOB | LONGBLOB | IMAGE | OID}
[( precisionInt )]
......
......@@ -3,7 +3,7 @@
02000=No data is available
07001=Invalid parameter count, expected count: {0}
08000=Error opening database
08004=Wrong user/password
08004=Wrong user name or password
21S02=Column count does not match
22003=Numeric value out of range
22012=Division by zero: {0}
......@@ -13,7 +13,7 @@
42000=Syntax error in SQL statement {0}
42001=Syntax error in SQL statement {0}; expected {1}
42S01=Table {0} already exists
42S02=Table {0} not found
42S02=Table {0} not found. Possible reasons: typo; the table is in another database or schema; case mismatch (use double quotes)
42S11=Index {0} already exists
42S12=Index {0} not found
42S21=Duplicate column name {0}
......@@ -21,18 +21,18 @@
42S32=Setting {0} not found
90000=Function {0} must return a result set
90001=Method is not allowed for a query
90002=Method is only allowed for a query
90001=Method is not allowed for a query. Use execute or executeQuery instead of executeUpdate
90002=Method is only allowed for a query. Use execute or executeUpdate instead of executeQuery
90003=Hexadecimal string with odd number of characters: {0}
90004=Hexadecimal string contains non hex character: {0}
90004=Hexadecimal string contains non-hex character: {0}
90005=Value too long for column {0}
90006=Null not allowed for column {0}
90006=NULL not allowed for column {0}
90007=The object is already closed
90008=Invalid value {0} for parameter {1}
90009=Cannot parse date constant {0}
90010=Cannot parse time constant {0}
90011=Cannot parse timestamp constant {0}
90012=Parameter number {0} is not set
90012=Parameter {0} is not set
90013=Database {0} not found
90014=Error parsing {0}
90015=SUM or AVG on wrong data type for {0}
......@@ -40,7 +40,7 @@
90017=Attempt to define a second primary key
90018=The connection was not closed by the application and is garbage collected
90019=Cannot drop the current user
90020=Database may be already open: {0}
90020=Database may be already in use: {0}. Possible solution: use the server mode
90021=Data conversion error converting {0}
90022=Function {0} not found
90023=Column {0} must not be nullable
......@@ -50,8 +50,8 @@
90027=Deserialization failed
90028=IO Exception: {0}
90029=Currently not on an updatable row
90030=File corrupted while reading record: {0}
90031=The connection was not closed
90030=File corrupted while reading record: {0}. Possible solution: use the recovery tool
90032=User {0} not found
90033=User {0} already exists
90034=Log file error: {0}
......
......@@ -31,7 +31,7 @@ class ResultDiskBuffer {
Database db = session.getDatabase();
rowBuff = DataPage.create(db, Constants.DEFAULT_DATA_PAGE_SIZE);
String fileName = session.getDatabase().createTempFile();
file = session.getDatabase().openFile(fileName, false);
file = session.getDatabase().openFile(fileName, "rw", false);
file.setCheckedWriting(false);
file.autoDelete();
file.seek(FileStore.HEADER_LENGTH);
......
......@@ -22,8 +22,8 @@ public class SecureFileStore extends FileStore {
private byte[] bufferForInitVector;
private int keyIterations;
public SecureFileStore(DataHandler handler, String name, byte[] magic, String cipher, byte[] key, int keyIterations) throws SQLException {
super(handler, name, magic);
public SecureFileStore(DataHandler handler, String name, String mode, byte[] magic, String cipher, byte[] key, int keyIterations) throws SQLException {
super(handler, name, mode, magic);
this.key = key;
if ("XTEA".equalsIgnoreCase(cipher)) {
this.cipher = new XTEA();
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论