Skip to content

Commit

Permalink
new TSqlDbConnectionProperties.EnsureColumnNameUnique property
Browse files Browse the repository at this point in the history
- by default, result column names won't be checked for name unicity
- it is up to your SQL statement to return genuine columns
- ColumName() lookup will use O(1) brute force, so you should rather use per-index Column*() methods
- force true to have the mORMot 1 compatible behavior, in which column names will be hashed for unicity (so will be slower at execution), but will allow faster ColumnName() lookup
  • Loading branch information
Arnaud Bouchez committed Jul 29, 2022
1 parent 2f060cf commit cb178e5
Show file tree
Hide file tree
Showing 11 changed files with 79 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/db/mormot.db.proxy.pas
Expand Up @@ -1339,7 +1339,7 @@ procedure TSqlDBProxyStatementAbstract.InternalHeaderProcess(
for F := 0 to colcount - 1 do
begin
FromVarString(Data, colname);
prop := fColumn.AddAndMakeUniqueName(colname);
prop := AddColumn(colname);
prop^.ColumnType := TSqlDBFieldType(Data^);
inc(Data);
prop^.ColumnValueDBSize := FromVarUInt32(Data);
Expand Down
6 changes: 2 additions & 4 deletions src/db/mormot.db.rad.pas
Expand Up @@ -592,14 +592,12 @@ procedure TSqlDBDatasetStatementAbstract.ExecutePrepared;
begin
fQuery.Open;
fCurrentRow := -1;
fColumnCount := 0;
fColumn.ForceReHash;
ClearColumns;
fColumn.Capacity := fQuery.FieldCount;
for i := 0 to fQuery.FieldCount - 1 do
begin
field := DatasetField(i);
with PSqlDBColumnProperty(fColumn.AddAndMakeUniqueName(
StringToUtf8(field.FieldName)))^ do
with AddColumn(StringToUtf8(field.FieldName))^ do
begin
ColumnAttr := PtrUInt(field);
ColumnType := ColumnTypeNativeToDB(field.DataType);
Expand Down
1 change: 1 addition & 0 deletions src/db/mormot.db.raw.sqlite3.pas
Expand Up @@ -4431,6 +4431,7 @@ TSqlRequest = record
Expand: boolean = false; aResultCount: PPtrInt = nil;
MaxMemory: PtrUInt = 512 shl 20; Options: TTextWriterOptions = []): RawUtf8;
/// Execute one SQL statement step into a JSON object
// - has less overhead than ExecuteJson() for a single row of data
function ExecuteStepJson(aDB: TSqlite3DB; W: TJsonWriter): boolean;
/// Execute one SQL statement which return the results as a TDocVariant array
// - if aSql is '', the statement should have been prepared, reset and bound
Expand Down
8 changes: 3 additions & 5 deletions src/db/mormot.db.sql.ibx.pas
Expand Up @@ -428,8 +428,7 @@ procedure TSqlDBIbxStatement.Prepare(const aSQL: RawUtf8; ExpectResults: boolean
tr, {$ifdef UNICODE} Utf8ToString(fSQL) {$else} fSQL {$endif});
fStatement.SetStaleReferenceChecks(false);
fStatement.SetRetainInterfaces(true);
fColumnCount := 0;
fColumn.ReHash;
ClearColumns;
fIbxParams := fStatement.GetSQLParams;
SetLength(farrParams, fIbxParams.Count);
for i := 0 to fIbxParams.Count - 1 do
Expand All @@ -448,9 +447,8 @@ procedure TSqlDBIbxStatement.Prepare(const aSQL: RawUtf8; ExpectResults: boolean
fColumnsMeta[i].Scale := fColumnMetaData.getScale;
fColumnsMeta[i].Subtype := fColumnMetaData.getSubtype;
name := fColumnMetaData.getName;
PSqlDBColumnProperty(fColumn.AddAndMakeUniqueName(
// Delphi<2009: already UTF-8 encoded due to controls_cp=CP_UTF8
{$ifdef UNICODE} StringToUtf8 {$endif}(name)))^.ColumnType :=
AddColumn(// Delphi<2009: already UTF-8 encoded due to controls_cp=CP_UTF8
{$ifdef UNICODE}StringToUtf8{$endif}(name))^.ColumnType :=
IbxSQLTypeToTSqlDBFieldType(fColumnsMeta[i]);
end;
end;
Expand Down
6 changes: 2 additions & 4 deletions src/db/mormot.db.sql.odbc.pas
Expand Up @@ -639,8 +639,7 @@ procedure TSqlDBOdbcStatement.BindColumns;
DescribeColW(fStatement, c, Name{%H-}, 256, NameLength, DataType,
ColumnSize, DecimalDigits, Nullable),
SQL_HANDLE_STMT, fStatement);
with PSqlDBColumnProperty(
fColumn.AddAndMakeUniqueName(RawUnicodeToUtf8(Name, NameLength)))^ do
with AddColumn(RawUnicodeToUtf8(Name, NameLength))^ do
begin
ColumnValueInlined := true;
ColumnValueDBType := DataType;
Expand Down Expand Up @@ -1270,8 +1269,7 @@ procedure TSqlDBOdbcStatement.ReleaseRows;
begin
if fStatement <> nil then
ODBC.CloseCursor(fStatement); // no check needed
fColumn.Clear;
fColumn.ForceReHash;
ClearColumns;
end;
inherited ReleaseRows;
end;
Expand Down
5 changes: 2 additions & 3 deletions src/db/mormot.db.sql.oracle.pas
Expand Up @@ -1916,8 +1916,7 @@ procedure TSqlDBOracleStatement.FreeHandles(AfterError: boolean);
end;
if fRowBuffer <> nil then
SetLength(fRowBuffer, 0); // release internal buffer memory
if fColumnCount > 0 then
fColumn.Clear;
ClearColumns;
end;

function TSqlDBOracleStatement.GetCol(Col: integer; out Column:
Expand Down Expand Up @@ -2006,7 +2005,7 @@ procedure TSqlDBOracleStatement.SetColumnsForPreparedStatement;
FastSetString(aName, oName, oNameLen);
AttrGet(oHandle, OCI_DTYPE_PARAM, @oType, nil, OCI_ATTR_DATA_TYPE, fError);
AttrGet(oHandle, OCI_DTYPE_PARAM, @oSize, nil, OCI_ATTR_DATA_SIZE, fError);
with PSqlDBColumnProperty(fColumn.AddAndMakeUniqueName(aName))^ do
with AddColumn(aName)^ do
begin
ColumnValueDBSize := oSize;
ColumnValueInlined := true;
Expand Down
65 changes: 57 additions & 8 deletions src/db/mormot.db.sql.pas
Expand Up @@ -1226,8 +1226,8 @@ TSqlDBConnectionProperties = class
fDbms: TSqlDBDefinition;
fBatchSendingAbilities: TSqlDBStatementCRUDs;
fUseCache, fStoreVoidStringAsNull, fLogSqlStatementOnException,
fRollbackOnDisconnect, fReconnectAfterConnectionError,
fFilterTableViewSchemaName: boolean;
fRollbackOnDisconnect, fReconnectAfterConnectionError,
fEnsureColumnNameUnique, fFilterTableViewSchemaName: boolean;
{$ifndef UNICODE}
fVariantWideString: boolean;
{$endif UNICODE}
Expand Down Expand Up @@ -1837,6 +1837,15 @@ TSqlDBConnectionProperties = class
// - is set to TRUE by default
property RollbackOnDisconnect: boolean
read fRollbackOnDisconnect write fRollbackOnDisconnect;
/// by default, result column names won't be checked for name unicity
// - it is up to your SQL statement to return genuine columns
// - ColumName() lookup will use O(1) brute force, so you should rather
// use per-index Column*() methods
// - force true to have the mORMot 1 compatible behavior, in which column
// names will be hashed for unicity (so will be slower at execution),
// but will allow faster ColumnName() lookup
property EnsureColumnNameUnique: boolean
read fEnsureColumnNameUnique write fEnsureColumnNameUnique;
/// defines if '' string values are to be stored as SQL null
// - by default, '' will be stored as ''
// - but some DB engines (e.g. Jet or MS SQL) does not allow by default to
Expand Down Expand Up @@ -1868,7 +1877,7 @@ TSqlDBConnectionProperties = class
property VariantStringAsWideString: boolean
read fVariantWideString write fVariantWideString;
{$endif UNICODE}
/// SQL statements what will be executed for each new connection
/// SQL statements what will be executed for each new connection, as typical
// usage scenarios examples:
// - Oracle: force case-insensitive like
// $ ['ALTER SESSION SET NLS_COMP=LINGUISTIC', 'ALTER SESSION SET NLS_SORT=BINARY_CI']
Expand Down Expand Up @@ -2914,6 +2923,8 @@ TSqlDBStatementWithParamsAndColumns = class(TSqlDBStatementWithParams)
protected
fColumns: TSqlDBColumnPropertyDynArray;
fColumn: TDynArrayHashed;
procedure ClearColumns;
function AddColumn(const ColName: RawUtf8): PSqlDBColumnProperty;
public
/// create a statement instance
// - this overridden version will initialize the internal fColumn* fields
Expand Down Expand Up @@ -3573,8 +3584,8 @@ function TSqlDBConnectionProperties.NewThreadSafeStatement: TSqlDBStatement;
result := ThreadSafeConnection.NewStatement;
end;

function TSqlDBConnectionProperties.NewThreadSafeStatementPrepared(const aSql:
RawUtf8; ExpectResults, RaiseExceptionOnError: boolean): ISqlDBStatement;
function TSqlDBConnectionProperties.NewThreadSafeStatementPrepared(
const aSql: RawUtf8; ExpectResults, RaiseExceptionOnError: boolean): ISqlDBStatement;
begin
result := ThreadSafeConnection.NewStatementPrepared(
aSql, ExpectResults, RaiseExceptionOnError);
Expand Down Expand Up @@ -8081,14 +8092,52 @@ constructor TSqlDBStatementWithParamsAndColumns.Create(
aConnection: TSqlDBConnection);
begin
inherited Create(aConnection);
fColumn.InitSpecific(TypeInfo(TSqlDBColumnPropertyDynArray),
fColumns, ptRawUtf8, @fColumnCount, {caseinsens=}true);
if aConnection.Properties.EnsureColumnNameUnique then
fColumn.InitSpecific(TypeInfo(TSqlDBColumnPropertyDynArray),
fColumns, ptRawUtf8, @fColumnCount, {caseinsens=}true)
else
PDynArray(@fColumn)^.Init(TypeInfo(TSqlDBColumnPropertyDynArray),
fColumns, @fColumnCount); // no hash index created nor unicity check
end;

procedure TSqlDBStatementWithParamsAndColumns.ClearColumns;
begin
if fColumnCount = 0 then
exit;
fColumn.Clear;
if fConnection.Properties.EnsureColumnNameUnique then
fColumn.ForceReHash;
end;

function TSqlDBStatementWithParamsAndColumns.AddColumn(
const ColName: RawUtf8): PSqlDBColumnProperty;
begin
if fConnection.Properties.EnsureColumnNameUnique then
result := fColumn.AddAndMakeUniqueName(ColName)
else
begin
result := PDynArray(@fColumn)^.NewPtr;
result^.ColumnName := ColName;
end;
end;

function TSqlDBStatementWithParamsAndColumns.ColumnIndex(
const aColumnName: RawUtf8): integer;
var
c: PSqlDBColumnProperty;
begin
result := fColumn.FindHashed(aColumnName);
if fConnection.Properties.EnsureColumnNameUnique then
result := fColumn.FindHashed(aColumnName)
else
begin
c := pointer(fColumns);
for result := 0 to fColumnCount - 1 do
if IdemPropNameU(aColumnName, c^.ColumnName) then
exit // brute force O(n) case insensitive check
else
inc(c);
result := -1
end;
end;

function TSqlDBStatementWithParamsAndColumns.ColumnName(Col: integer): RawUtf8;
Expand Down
9 changes: 5 additions & 4 deletions src/db/mormot.db.sql.postgres.pas
Expand Up @@ -526,15 +526,16 @@ procedure TSqlDBPostgresStatement.BindColumns;
var
nCols, c: integer;
cName: RawUtf8;
p: PUtf8Char;
begin
fColumn.Clear;
fColumn.ForceReHash;
ClearColumns;
nCols := PQ.nfields(fRes);
fColumn.Capacity := nCols;
for c := 0 to nCols - 1 do
begin
cName := PQ.fname(fRes, c);
with PSqlDBColumnProperty(fColumn.AddAndMakeUniqueName(cName))^ do
p := PQ.fname(fRes, c);
FastSetString(cName, p, StrLen(p));
with AddColumn(cName)^ do
begin
ColumnAttr := PQ.ftype(fRes, c);
ColumnType := TSqlDBPostgresConnectionProperties(Connection.Properties).
Expand Down
8 changes: 3 additions & 5 deletions src/db/mormot.db.sql.zeos.pas
Expand Up @@ -1137,8 +1137,7 @@ procedure TSqlDBZeosStatement.ExecutePrepared;
// 2. Execute query
if fExpectResults then
begin
fColumnCount := 0;
fColumn.ForceReHash;
ClearColumns;
fCurrentRow := -1;
fResultSet := fStatement.ExecuteQueryPrepared;
if fResultSet = nil then
Expand All @@ -1158,9 +1157,8 @@ procedure TSqlDBZeosStatement.ExecutePrepared;
name := fResultInfo.GetColumnLabel(i + FirstDbcIndex);
if name = '' then
name := fResultInfo.GetColumnName(i + FirstDbcIndex);
PSqlDBColumnProperty(fColumn.AddAndMakeUniqueName(
// Delphi<2009: already UTF-8 encoded due to controls_cp=CP_UTF8
{$ifdef UNICODE}StringToUtf8{$endif}(name)))^.ColumnType :=
AddColumn(// Delphi<2009: already UTF-8 encoded due to controls_cp=CP_UTF8
{$ifdef UNICODE}StringToUtf8{$endif}(name))^.ColumnType :=
Props.TZSQLTypeToTSqlDBFieldType(fResultInfo.GetColumnType(i + FirstDbcIndex));
end;
end;
Expand Down
2 changes: 1 addition & 1 deletion src/mormot.commit.inc
@@ -1 +1 @@
'2.0.3819'
'2.0.3820'
4 changes: 2 additions & 2 deletions test/test.orm.sqlite3.pas
Expand Up @@ -2068,8 +2068,8 @@ procedure TTestSQLite3Engine._TOrmTableJson;
if JS <> '' then // avoid memory leak
with TOrmTableDB.Create(Demo, [], Req, {expand=}true) do
try
check(RowCount = J.RowCount);
check(FieldCount = J.FieldCount);
checkEqual(RowCount, J.RowCount);
checkEqual(FieldCount, J.FieldCount);
SetFieldType('YearOfBirth', oftModTime);
for aR := 0 to RowCount do
for aF := 0 to FieldCount - 1 do
Expand Down

0 comments on commit cb178e5

Please sign in to comment.