feat(contrib/drivers/dm): add Replace/InsertIgnore support for dm

This commit is contained in:
John Guo
2025-12-04 17:29:39 +08:00
parent 48845c3473
commit 63c2bb7c86
10 changed files with 1660 additions and 293 deletions

View File

@ -1,4 +1,3 @@
English | [简体中文](README.zh_CN.MD)
# Database drivers
@ -44,7 +43,7 @@ func main() {
## Supported Drivers
### MySQL/MariaDB/TiDB
### MySQL/MariaDB/TiDB/OceanBase
```go
import _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
@ -116,10 +115,6 @@ Note:
import _ "github.com/gogf/gf/contrib/drivers/dm/v2"
```
Note:
- It does not support `Replace` features.
## Custom Drivers
It's quick and easy, please refer to current driver source.

View File

@ -1,126 +0,0 @@
[English](README.MD) | 简体中文
# 数据库驱动程序
用于gdb包的数据库驱动程序。
## 安装
以 `mysql` 为例。
```shell
go get github.com/gogf/gf/contrib/drivers/mysql/v2@latest
# 方便复制
go get github.com/gogf/gf/contrib/drivers/clickhouse/v2@latest
go get github.com/gogf/gf/contrib/drivers/dm/v2@latest
go get github.com/gogf/gf/contrib/drivers/mssql/v2@latest
go get github.com/gogf/gf/contrib/drivers/oracle/v2@latest
go get github.com/gogf/gf/contrib/drivers/pgsql/v2@latest
go get github.com/gogf/gf/contrib/drivers/sqlite/v2@latest
go get github.com/gogf/gf/contrib/drivers/sqlitecgo/v2@latest
```
选择并将驱动程序导入到您的项目中:
```go
import _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
```
通常在 `main.go` 的顶部导入:
```go
package main
import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
// 其他导入的包。
)
func main() {
// 主要逻辑。
}
```
## 支持的驱动程序
### MySQL/MariaDB/TiDB
```go
import _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
```
### SQLite
```go
import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
```
#### cgo 版本
32位Windows请使用cgo版本
```go
import _ "github.com/gogf/gf/contrib/drivers/sqlitecgo/v2"
```
### PostgreSQL
```go
import _ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
```
注意:
- 不支持 `Replace` 功能。
### SQL Server
```go
import _ "github.com/gogf/gf/contrib/drivers/mssql/v2"
```
注意:
- 不支持 `Replace` 功能。
- 仅支持服务器版本 >= `SQL Server2005`
- 仅支持 datetime2 和 datetimeoffset 类型来自动处理 created_at/updated_at/deleted_at 列,因为 datetime 类型在将列值作为字符串传递时不支持微秒精度。
### Oracle
```go
import _ "github.com/gogf/gf/contrib/drivers/oracle/v2"
```
注意:
- 不支持 `Replace` 功能。
- 不支持 `LastInsertId`。
### ClickHouse
```go
import _ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
```
注意:
- 不支持 `InsertIgnore/InsertGetId` 功能。
- 不支持 `Save/Replace` 功能。
- 不支持 `Transaction` 功能。
- 不支持 `RowsAffected` 功能。
### DM
```go
import _ "github.com/gogf/gf/contrib/drivers/dm/v2"
```
注意:
- 不支持 `Replace` 功能。
## 自定义驱动程序
自定义驱动程序非常快速和简单,您可以参考当前驱动程序的源代码来进行开发。
如果您有关于支持新驱动程序的PRPull Request我们将非常感激地接受您的提交到当前仓库。

View File

@ -14,6 +14,7 @@ import (
"github.com/gogf/gf/v2/frame/g"
)
// Driver is the driver for dm database.
type Driver struct {
*gdb.Core
}

View File

@ -28,28 +28,70 @@ func (d *Driver) DoInsert(
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionReplace:
// TODO:: Should be Supported
return nil, gerror.NewCode(
gcode.CodeNotSupported, `Replace operation is not supported by dm driver`,
)
}
// dm does not support REPLACE INTO syntax, use SAVE instead.
return d.doSave(ctx, link, table, list, option)
return d.Core.DoInsert(ctx, link, table, list, option)
case gdb.InsertOptionIgnore:
// dm does not support INSERT IGNORE syntax, use MERGE instead.
return d.doInsertIgnore(ctx, link, table, list, option)
default:
return d.Core.DoInsert(ctx, link, table, list, option)
}
}
// doSave support upsert for dm
func (d *Driver) doSave(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
if len(option.OnConflict) == 0 {
return nil, gerror.NewCode(
gcode.CodeMissingParameter, `Please specify conflict columns`,
)
return d.doMergeInsert(ctx, link, table, list, option, true)
}
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for DM database.
// It only inserts records when there's no conflict on primary/unique keys.
func (d *Driver) doInsertIgnore(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, false)
}
// doMergeInsert implements MERGE-based insert operations for DM database.
// When withUpdate is true, it performs upsert (insert or update).
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
func (d *Driver) doMergeInsert(
ctx context.Context,
link gdb.Link,
table string,
list gdb.List,
option gdb.DoInsertOption,
withUpdate bool,
) (result sql.Result, err error) {
// If OnConflict is not specified, automatically get the primary key of the table
conflictKeys := option.OnConflict
if len(conflictKeys) == 0 {
conflictKeys, err = d.getPrimaryKeys(ctx, table)
if err != nil {
return nil, gerror.WrapCode(
gcode.CodeInternalError,
err,
`failed to get primary keys for table`,
)
}
if len(conflictKeys) == 0 {
return nil, gerror.NewCode(
gcode.CodeMissingParameter,
`Please specify conflict columns or ensure the table has a primary key`,
)
}
}
if len(list) == 0 {
return nil, gerror.NewCode(
gcode.CodeInvalidRequest, `Save operation list is empty by oracle driver`,
opName := "Save"
if !withUpdate {
opName = "InsertIgnore"
}
return nil, gerror.NewCodef(
gcode.CodeInvalidRequest, `%s operation list is empty by dm driver`, opName,
)
}
@ -58,14 +100,13 @@ func (d *Driver) doSave(ctx context.Context,
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeys = option.OnConflict
conflictKeySet = gset.New(false)
// queryHolders: Handle data with Holder that need to be upsert
// queryValues: Handle data that need to be upsert
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged
// insertKeys: Handle valid keys that need to be inserted
// insertValues: Handle values that need to be inserted
// updateValues: Handle values that need to be updated
// updateValues: Handle values that need to be updated (only when withUpdate=true)
queryHolders = make([]string, oneLen)
queryValues = make([]any, oneLen)
insertKeys = make([]string, oneLen)
@ -86,9 +127,9 @@ func (d *Driver) doSave(ctx context.Context,
insertKeys[index] = keyWithChar
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
// filter conflict keys in updateValues.
// And the key is not a soft created field.
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
// Build updateValues only when withUpdate is true
// Filter conflict keys and soft created fields from updateValues
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
@ -97,8 +138,10 @@ func (d *Driver) doSave(ctx context.Context,
index++
}
batchResult := new(gdb.SqlResult)
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
var (
batchResult = new(gdb.SqlResult)
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
)
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
if err != nil {
return r, err
@ -112,40 +155,59 @@ func (d *Driver) doSave(ctx context.Context,
return batchResult, nil
}
// parseSqlForUpsert
// MERGE INTO {{table}} T1
// USING ( SELECT {{queryHolders}} FROM DUAL T2
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
// WHEN NOT MATCHED THEN
// INSERT {{insertKeys}} VALUES {{insertValues}}
// WHEN MATCHED THEN
// UPDATE SET {{updateValues}}
func parseSqlForUpsert(table string,
// getPrimaryKeys retrieves the primary key field list of the table.
// This method extracts primary key information from TableFields.
func (d *Driver) getPrimaryKeys(ctx context.Context, table string) ([]string, error) {
tableFields, err := d.TableFields(ctx, table)
if err != nil {
return nil, err
}
var primaryKeys []string
for _, field := range tableFields {
if field.Key == "PRI" {
primaryKeys = append(primaryKeys, field.Name)
}
}
return primaryKeys, nil
}
// parseSqlForMerge generates MERGE statement for DM database.
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
// Examples:
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
func parseSqlForMerge(table string,
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
) (sqlStr string) {
var (
queryHolderStr = strings.Join(queryHolders, ",")
insertKeyStr = strings.Join(insertKeys, ",")
insertValueStr = strings.Join(insertValues, ",")
updateValueStr = strings.Join(updateValues, ",")
duplicateKeyStr string
pattern = gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s;`)
)
// Build ON condition
for index, keys := range duplicateKey {
if index != 0 {
duplicateKeyStr += " AND "
}
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
duplicateKeyStr += duplicateTmp
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
}
return fmt.Sprintf(pattern,
table,
queryHolderStr,
duplicateKeyStr,
insertKeyStr,
insertValueStr,
updateValueStr,
)
// Build SQL based on whether UPDATE is needed
pattern := gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`)
if len(updateValues) > 0 {
// Upsert: INSERT or UPDATE
pattern += gstr.Trim(`WHEN MATCHED THEN UPDATE SET %s`)
return fmt.Sprintf(
pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr,
strings.Join(updateValues, ","),
)
} else {
// Insert Ignore: INSERT only
return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr)
}
}

View File

@ -23,7 +23,7 @@ func escapeSingleQuote(s string) string {
}
const (
tableFieldsSqlTmp = `SELECT c.COLUMN_NAME, c.DATA_TYPE, c.DATA_DEFAULT, c.NULLABLE, cc.COMMENTS FROM ALL_TAB_COLUMNS c LEFT JOIN ALL_COL_COMMENTS cc ON c.COLUMN_NAME = cc.COLUMN_NAME AND c.TABLE_NAME = cc.TABLE_NAME AND c.OWNER = cc.OWNER WHERE c.TABLE_NAME = '%s' AND c.OWNER = '%s'`
tableFieldsSqlTmp = `SELECT c.COLUMN_NAME, c.DATA_TYPE, c.DATA_LENGTH, c.DATA_PRECISION, c.DATA_SCALE, c.DATA_DEFAULT, c.NULLABLE, cc.COMMENTS FROM ALL_TAB_COLUMNS c LEFT JOIN ALL_COL_COMMENTS cc ON c.COLUMN_NAME = cc.COLUMN_NAME AND c.TABLE_NAME = cc.TABLE_NAME AND c.OWNER = cc.OWNER WHERE c.TABLE_NAME = '%s' AND c.OWNER = '%s'`
tableFieldsPkSqlSchemaTmp = `SELECT COLS.COLUMN_NAME AS PRIMARY_KEY_COLUMN FROM USER_CONSTRAINTS CONS JOIN USER_CONS_COLUMNS COLS ON CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME WHERE CONS.TABLE_NAME = '%s' AND CONS.CONSTRAINT_TYPE = 'P'`
tableFieldsPkSqlDBATmp = `SELECT COLS.COLUMN_NAME AS PRIMARY_KEY_COLUMN FROM DBA_CONSTRAINTS CONS JOIN DBA_CONS_COLUMNS COLS ON CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME WHERE CONS.TABLE_NAME = '%s' AND CONS.OWNER = '%s' AND CONS.CONSTRAINT_TYPE = 'P'`
)
@ -85,10 +85,24 @@ func (d *Driver) TableFields(
if m["NULLABLE"].String() != "N" {
nullable = true
}
// Build field type with length/precision
// For NUMBER(p,s): use DATA_PRECISION and DATA_SCALE
// For VARCHAR2/CHAR: use DATA_LENGTH
var (
fieldType string
dataType = m["DATA_TYPE"].String()
dataLength = m["DATA_LENGTH"].Int()
)
if dataLength > 0 {
fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength)
} else {
fieldType = dataType
}
fields[m["COLUMN_NAME"].String()] = &gdb.TableField{
Index: i,
Name: m["COLUMN_NAME"].String(),
Type: m["DATA_TYPE"].String(),
Type: fieldType,
Null: nullable,
Default: m["DATA_DEFAULT"].Val(),
Key: pkFields.Get(m["COLUMN_NAME"].String()),

View File

@ -80,12 +80,12 @@ func TestTableFields(t *testing.T) {
createInitTable(tables)
gtest.C(t, func(t *gtest.T) {
var expect = map[string][]any{
"ID": {"BIGINT", false},
"ACCOUNT_NAME": {"VARCHAR", false},
"PWD_RESET": {"TINYINT", false},
"ATTR_INDEX": {"INT", true},
"DELETED": {"INT", false},
"CREATED_TIME": {"TIMESTAMP", false},
"ID": {"BIGINT(8)", false},
"ACCOUNT_NAME": {"VARCHAR(128)", false},
"PWD_RESET": {"TINYINT(1)", false},
"ATTR_INDEX": {"INT(4)", true},
"DELETED": {"INT(4)", false},
"CREATED_TIME": {"TIMESTAMP(8)", false},
}
res, err := db.TableFields(ctx, tables)
@ -140,109 +140,6 @@ func Test_DB_Query(t *testing.T) {
})
}
func TestModelSave(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
AccountName string
AttrIndex int
}
var (
user User
count int
result sql.Result
err error
)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"accountName": "ac1",
"attrIndex": 100,
}).OnConflict("id").Save()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.Id, 1)
t.Assert(user.AccountName, "ac1")
t.Assert(user.AttrIndex, 100)
_, err = db.Model(table).Data(g.Map{
"id": 1,
"accountName": "ac2",
"attrIndex": 200,
}).OnConflict("id").Save()
t.AssertNil(err)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.AccountName, "ac2")
t.Assert(user.AttrIndex, 200)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}
func TestModelInsert(t *testing.T) {
// g.Model.insert not lost default not null coloumn
table := "A_tables"
createInitTable(table)
gtest.C(t, func(t *gtest.T) {
i := 200
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwo`, i),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
_, err := db.Model(table).Insert(&data)
gtest.AssertNil(err)
})
gtest.C(t, func(t *gtest.T) {
i := 201
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwoONE`, i),
PwdReset: 1,
CreatedTime: time.Now(),
AttrIndex: 98,
UpdatedTime: time.Now(),
}
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
_, err := db.Model(table).Data(&data).Insert()
gtest.AssertNil(err)
})
}
func TestDBInsert(t *testing.T) {
table := "A_tables"
createInitTable("A_tables")
gtest.C(t, func(t *gtest.T) {
i := 300
data := g.Map{
"ID": i,
"ACCOUNT_NAME": fmt.Sprintf(`A%dthress`, i),
"PWD_RESET": 3,
"ATTR_INDEX": 98,
"CREATED_TIME": gtime.Now(),
"UPDATED_TIME": gtime.Now(),
}
_, err := db.Insert(ctx, table, &data)
gtest.AssertNil(err)
})
}
func Test_DB_Exec(t *testing.T) {
createInitTable("A_tables")
gtest.C(t, func(t *gtest.T) {
@ -612,3 +509,124 @@ func Test_Empty_Slice_Argument(t *testing.T) {
t.Assert(len(result), 0)
})
}
func TestModelSave(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
AccountName string
AttrIndex int
}
var (
user User
count int
result sql.Result
err error
)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"accountName": "ac1",
"attrIndex": 100,
}).OnConflict("id").Save()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.Id, 1)
t.Assert(user.AccountName, "ac1")
t.Assert(user.AttrIndex, 100)
_, err = db.Model(table).Data(g.Map{
"id": 1,
"accountName": "ac2",
"attrIndex": 200,
}).OnConflict("id").Save()
t.AssertNil(err)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.AccountName, "ac2")
t.Assert(user.AttrIndex, 200)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}
func TestModelInsert(t *testing.T) {
// g.Model.insert not lost default not null coloumn
table := "A_tables"
createInitTable(table)
gtest.C(t, func(t *gtest.T) {
i := 200
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwo`, i),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
_, err := db.Model(table).Insert(&data)
gtest.AssertNil(err)
})
gtest.C(t, func(t *gtest.T) {
i := 201
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwoONE`, i),
PwdReset: 1,
CreatedTime: time.Now(),
AttrIndex: 98,
UpdatedTime: time.Now(),
}
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
_, err := db.Model(table).Data(&data).Insert()
gtest.AssertNil(err)
})
}
func Test_Model_InsertIgnore(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// db.SetDebug(true)
gtest.C(t, func(t *gtest.T) {
data := User{
ID: int64(666),
AccountName: fmt.Sprintf(`name_%d`, 666),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
_, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
})
gtest.C(t, func(t *gtest.T) {
data := User{
ID: int64(666),
AccountName: fmt.Sprintf(`name_%d`, 777),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNil(err)
one, err := db.Model(table).Where("id", 666).One()
t.AssertNil(err)
t.Assert(one["ACCOUNT_NAME"].String(), "name_666")
})
}

File diff suppressed because it is too large Load Diff

View File

@ -63,8 +63,8 @@ func init() {
Weight: 1,
MaxIdleConnCount: 10,
MaxOpenConnCount: 10,
CreatedAt: "created_time",
UpdatedAt: "updated_time",
// CreatedAt: "created_time",
// UpdatedAt: "updated_time",
}
nodeLink := gdb.ConfigNode{

View File

@ -446,8 +446,10 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List,
// Group the list by fields. Different fields to different list.
// It here uses ListMap to keep sequence for data inserting.
// ============================================================================================
var keyListMap = gmap.NewListMap()
var tmpkeyListMap = make(map[string]List)
var (
keyListMap = gmap.NewListMap()
tmpKeyListMap = make(map[string]List)
)
for _, item := range list {
mapLen := len(item)
if mapLen == 0 {
@ -463,13 +465,13 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List,
keys = tmpKeys // for fieldsToSequence
tmpKeysInSequenceStr := gstr.Join(tmpKeys, ",")
if tmpkeyListMapItem, ok := tmpkeyListMap[tmpKeysInSequenceStr]; ok {
tmpkeyListMap[tmpKeysInSequenceStr] = append(tmpkeyListMapItem, item)
if tmpKeyListMapItem, ok := tmpKeyListMap[tmpKeysInSequenceStr]; ok {
tmpKeyListMap[tmpKeysInSequenceStr] = append(tmpKeyListMapItem, item)
} else {
tmpkeyListMap[tmpKeysInSequenceStr] = List{item}
tmpKeyListMap[tmpKeysInSequenceStr] = List{item}
}
}
for tmpKeysInSequenceStr, itemList := range tmpkeyListMap {
for tmpKeysInSequenceStr, itemList := range tmpKeyListMap {
keyListMap.Set(tmpKeysInSequenceStr, itemList)
}
if keyListMap.Size() > 1 {

View File

@ -380,6 +380,7 @@ func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate(
ctx context.Context, fieldType LocalType, isDeletedField bool,
) any {
var value any
// for creat or update procedure, the deleted field is always set to non-deleted value.
if isDeletedField {
switch fieldType {
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime: