Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/test-results/2026-09-26-type-connection-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@
## 后续修复:连接消息压缩

`compress_msg` 原先被静默忽略;现在只接受 `DSQL_FALSE`/`DSQL_TRUE`,将选择传给 Go 驱动并从协商后的连接状态读回。两种取值均已在真实 DM8 上建连和查询成功。底层 Go 驱动只解析 `StmtPoolSize`,没有使用对应缓存大小;因此显式请求 `use_stmt_pool=1` 现在会报不支持,`use_stmt_pool=0` 可用。语句句柄缓存仍待实现。

## 后续回归:复杂对象与数组

[对象数组回归](2026-09-27-complex-arrays.md)新增嵌套对象的空值、对象数组中的高精度十进制与空元素,以及数组嵌套数组的往返。修复了桥接层拒绝对象数组、数组位置错误复用元素描述,以及底层 Go 驱动对嵌套数组指针输入发生进程崩溃的问题。
23 changes: 23 additions & 0 deletions docs/test-results/2026-09-27-complex-arrays.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Complex object and array regression (2026-09-27)

The bridge now describes user-defined object elements inside VARRAY values
through `ALL_TYPES`, without requiring access to `SYS.SYSOBJECTS`. It also
binds nested array pointers correctly and returns errors for malformed complex
elements instead of terminating the Python process.

The first GitHub ARM run exposed an additional fetch bug: the C extension
passes an uninitialized nested object handle slot. The bridge now checks for
a live object handle before reusing that slot; the complete CI rerun is the
acceptance gate for this Linux-specific path.

Real DM8 ARM verification:

- GB18030: complete suite **191 passed**, 6 non-database tests deselected.
- UTF8: object and array suite **12 passed**.
- New cases cover nullable fields in nested objects, VARRAY elements that are
objects (including a null object and `DECIMAL(30,8)`), and nested VARRAYs
containing high-precision decimals and null elements.

The vendored Go tests also exercise malformed nested elements and the modern
SSL CA/hostname validation path. Cross-schema object arrays and other DM8
server versions still need dedicated regression environments.
3 changes: 2 additions & 1 deletion dpi_bridge/dpi_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,8 @@ func writeValueToBinding(val interface{}, bind bindColInfo, sqlType int16, objec
writeLobHandleValue(val, bind, sqlType)
case DSQL_C_CLASS, DSQL_C_RECORD, DSQL_C_ARRAY, DSQL_C_SARRAY:
hobj := *(*C.dhobj)(bind.dataPtr)
if hobj == nil {
stored, valid := getHandle(ptrToHandle(unsafe.Pointer(hobj)))
if _, isObject := stored.(*objHandle); !valid || !isObject {
id := allocHandle(&objHandle{})
hobj = C.dhobj(handleToPtr(id))
*(*C.dhobj)(bind.dataPtr) = hobj
Expand Down
40 changes: 32 additions & 8 deletions dpi_bridge/dpi_obj.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func describeObjectType(conn *connHandle, owner, typeName string, visiting map[s
case "RECORD":
desc.sqlType = DSQL_RECORD
case "TYPE":
return describeArrayType(conn, desc)
return describeArrayType(conn, desc, visiting)
default:
return nil, fmt.Errorf("object type %s has unsupported kind %s", key, typeCode)
}
Expand Down Expand Up @@ -194,7 +194,7 @@ func describeObjectType(conn *connHandle, owner, typeName string, visiting map[s
return desc, nil
}

func describeArrayType(conn *connHandle, desc *objDescHandle) (*objDescHandle, error) {
func describeArrayType(conn *connHandle, desc *objDescHandle, visiting map[string]bool) (*objDescHandle, error) {
if conn.conn == nil {
return nil, fmt.Errorf("not connected")
}
Expand All @@ -210,11 +210,28 @@ func describeArrayType(conn *connHandle, desc *objDescHandle) (*objDescHandle, e
default:
return nil, fmt.Errorf("type %s.%s is not an array (driver kind %d, element %d, limit %d)", desc.schema, desc.name, info.Kind, info.ElementKind, info.MaxElements)
}
if info.ElementKind == dm.ARRAY || info.ElementKind == dm.SARRAY || info.ElementKind == dm.CLASS || info.ElementKind == dm.PLTYPE_RECORD {
return nil, fmt.Errorf("array %s.%s has unsupported complex elements", desc.schema, desc.name)
}
field := objFieldDesc{sqlType: int16(info.ElementKind), precision: int16(info.ElementPrecision), scale: int16(info.ElementScale)}
if !supportedArrayElement(field.sqlType) {
if info.ElementKind == dm.CLASS || info.ElementKind == dm.PLTYPE_RECORD {
if info.ElementTypeName == "" {
return nil, fmt.Errorf("array %s.%s has unnamed object elements", desc.schema, desc.name)
}
field.schema, field.typeName = desc.schema, info.ElementTypeName
if info.ElementTypeOID > 0 {
var visibleOwner, visibleName string
err = conn.db.QueryRow("SELECT OWNER, TYPE_NAME FROM ALL_TYPES WHERE TYPE_OID=?", info.ElementTypeOID).Scan(&visibleOwner, &visibleName)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if err == nil {
field.schema, field.typeName = visibleOwner, visibleName
}
}
field.nested, err = describeObjectType(conn, field.schema, field.typeName, visiting)
if err != nil {
return nil, err
}
field.sqlType = field.nested.sqlType
} else if !supportedArrayElement(field.sqlType) {
return nil, fmt.Errorf("array %s.%s has unsupported element type %d", desc.schema, desc.name, info.ElementKind)
}
desc.fields = []objFieldDesc{field}
Expand Down Expand Up @@ -528,13 +545,20 @@ func dpi_get_obj_attr(hobj C.dhobj, nth C.udint4, attrID C.udint2, buf C.dpointe
//export dpi_get_obj_desc_attr
func dpi_get_obj_desc_attr(objDesc C.dhobjdesc, nth C.udint4, attrID C.udint2, buf C.dpointer, bufLen C.udint4, length *C.slength) C.DPIRETURN {
desc, ok := objectDescriptor(objDesc)
if !ok || buf == nil || int(nth) > len(desc.fields) {
if !ok || buf == nil {
return DSQL_INVALID_HANDLE
}
fieldIndex := int(nth) - 1
if nth > 0 && (desc.sqlType == DSQL_ARRAY || desc.sqlType == DSQL_SARRAY) {
fieldIndex = 0
}
if nth > 0 && fieldIndex >= len(desc.fields) {
return DSQL_INVALID_HANDLE
}
name, schema, sqlType, precision, scale, count := desc.name, desc.schema, desc.sqlType, int16(0), int16(0), len(desc.fields)
var field *objFieldDesc
if nth > 0 {
field = &desc.fields[int(nth)-1]
field = &desc.fields[fieldIndex]
name, schema, sqlType, precision, scale, count = field.name, field.schema, field.sqlType, field.precision, field.scale, 0
if field.nested != nil {
count = len(field.nested.fields)
Expand Down
21 changes: 21 additions & 0 deletions dpi_bridge/third_party/chunanyong_dm/PATCHES.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,27 @@
- Regression: `tests/integration/test_p1_array_types.py` covers high-precision
decimals, Unicode, nulls, empty arrays, length limits, and arrays in objects.

## Patch: describe complex array elements and reject invalid nested values

- Files: `type_info.go`, `zzo.go`
- Expose the element type name and OID without querying privileged
`SYS.SYSOBJECTS`; the bridge resolves visible object types through
`ALL_TYPES` and supplies nested descriptors for arrays of objects.
- The bridge treats nested fetch handle slots as outputs and allocates a fresh
handle when a slot contains no live object handle. This avoids depending on
uninitialized C stack values, which failed on Linux ARM runners.
- Encode pointer-valued nested arrays and reject malformed complex elements
instead of panicking on an invalid type assertion or nil slice index.
- Regression: `tests/integration/test_p1_array_types.py` covers an array of
nullable objects and nested arrays with `DECIMAL(30,8)`; `complex_array_test.go`
checks malformed nested values.

## Patch: verify modern SSL certificates

- File: `security/zzi_test.go`
- Local TLS handshakes verify CA trust and DNS/IP SAN matching, including a
bracketed IPv6 address, and reject a wrong hostname or untrusted CA.

## Patch: resolve service names with the bridge's default port

- Files: `n.go`, `zzm.go`
Expand Down
18 changes: 18 additions & 0 deletions dpi_bridge/third_party/chunanyong_dm/complex_array_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dm

import "testing"

func TestComplexArrayRejectsInvalidElementWithoutPanic(t *testing.T) {
for _, kind := range []int{CLASS, ARRAY} {
root := newTypeDescriptor(nil)
root.m_arrObj = newTypeDescriptor(nil)
root.m_arrObj.column.colType = int32(kind)
if kind == ARRAY {
root.m_arrObj.m_arrObj = newTypeDescriptor(nil)
root.m_arrObj.m_arrObj.column.colType = INT
}
if _, err := TypeDataSV.toArray([]interface{}{"invalid"}, root); err == nil {
t.Errorf("kind %d accepted an invalid nested element", kind)
}
}
}
137 changes: 137 additions & 0 deletions dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package security

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

func testCertificate(t *testing.T, template, parent *x509.Certificate, signer *ecdsa.PrivateKey) ([]byte, *ecdsa.PrivateKey) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
if signer == nil {
signer = key
}
der, err := x509.CreateCertificate(rand.Reader, template, parent, &key.PublicKey, signer)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), key
}

func testCA(t *testing.T) ([]byte, *x509.Certificate, *ecdsa.PrivateKey) {
t.Helper()
now := time.Now()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1), NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour),
IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
cert, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), cert, key
}

func writeTestFile(t *testing.T, path string, data []byte) {
t.Helper()
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
}

func testTLSFiles(t *testing.T) (string, tls.Certificate) {
t.Helper()
dir := t.TempDir()
caPEM, ca, caKey := testCA(t)
writeTestFile(t, filepath.Join(dir, "ca-cert.pem"), caPEM)
leaf := &x509.Certificate{
SerialNumber: big.NewInt(2), NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP("::1")},
KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
leafPEM, leafKey := testCertificate(t, leaf, ca, caKey)
keyDER, err := x509.MarshalECPrivateKey(leafKey)
if err != nil {
t.Fatal(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
writeTestFile(t, filepath.Join(dir, "client-cert.pem"), leafPEM)
writeTestFile(t, filepath.Join(dir, "client-key.pem"), keyPEM)
serverCert, err := tls.X509KeyPair(leafPEM, keyPEM)
if err != nil {
t.Fatal(err)
}
return dir, serverCert
}

func testTLSHandshake(t *testing.T, dir string, serverCert tls.Certificate, serverName string) error {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
serverDone := make(chan error, 1)
go func() {
serverSocket, acceptErr := listener.Accept()
if acceptErr != nil {
serverDone <- acceptErr
return
}
server := tls.Server(serverSocket, &tls.Config{Certificates: []tls.Certificate{serverCert}, MinVersion: tls.VersionTLS12})
serverDone <- server.Handshake()
server.Close()
}()
clientSocket, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
client, err := NewTLSFromTCP(clientSocket, filepath.Join(dir, "client-cert.pem"), filepath.Join(dir, "client-key.pem"), dir, serverName)
if client != nil {
client.Close()
} else {
clientSocket.Close()
}
<-serverDone
return err
}

func TestModernServerCertificateVerification(t *testing.T) {
dir, serverCert := testTLSFiles(t)
if err := testTLSHandshake(t, dir, serverCert, "localhost"); err != nil {
t.Fatalf("trusted DNS name: %v", err)
}
if err := testTLSHandshake(t, dir, serverCert, "[::1]"); err != nil {
t.Fatalf("trusted bracketed IPv6: %v", err)
}
if err := testTLSHandshake(t, dir, serverCert, "wrong.example"); err == nil || !strings.Contains(err.Error(), "not wrong.example") {
t.Fatalf("wrong hostname should fail verification, got %v", err)
}
untrustedCA, _, _ := testCA(t)
writeTestFile(t, filepath.Join(dir, "ca-cert.pem"), untrustedCA)
if err := testTLSHandshake(t, dir, serverCert, "localhost"); err == nil || !strings.Contains(err.Error(), "unknown authority") {
t.Fatalf("untrusted CA should fail verification, got %v", err)
}
}
12 changes: 12 additions & 0 deletions dpi_bridge/third_party/chunanyong_dm/type_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package dm
type ArrayTypeInfo struct {
Kind int
ElementKind int
ElementTypeName string
ElementTypeOID int
ElementPrecision int
ElementScale int
MaxElements int
Expand All @@ -29,9 +31,19 @@ func DescribeArrayType(conn *DmConnection, typeName string) (ArrayTypeInfo, erro
if kind == SARRAY {
maxElements = root.getStaticArrayLength()
}
elementTypeName := ""
elementTypeOID := 0
if item.getDType() == CLASS || item.getDType() == PLTYPE_RECORD ||
item.getDType() == ARRAY || item.getDType() == SARRAY {
// getFulName queries SYS.SYSOBJECTS, which regular accounts cannot read.
elementTypeName = item.m_sqlName.m_name
elementTypeOID = item.getObjId()
}
return ArrayTypeInfo{
Kind: kind,
ElementKind: item.getDType(),
ElementTypeName: elementTypeName,
ElementTypeOID: elementTypeOID,
ElementPrecision: item.getPrec(),
ElementScale: item.getScale(),
MaxElements: maxElements,
Expand Down
27 changes: 21 additions & 6 deletions dpi_bridge/third_party/chunanyong_dm/zzo.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,24 @@ func (sv TypeData) toArray(objArr []interface{}, desc *TypeDescriptor) ([]TypeDa
continue
}

switch objArr[i].(type) {
case DmStruct, DmArray:
retData[i] = *newTypeData(objArr[i], nil)
switch value := objArr[i].(type) {
case *DmArray:
array, err := value.createByArrayDescriptor(
newArrayDescriptorByTypeDescriptor(desc.m_arrObj), desc.m_conn)
if err != nil {
return nil, err
}
retData[i] = *newTypeData(array, nil)
case DmStruct, DmArray, *DmStruct:
retData[i] = *newTypeData(value, nil)
default:
switch desc.m_arrObj.getDType() {
case CLASS, PLTYPE_RECORD:
tdArr, err := sv.toStruct(objArr[i].([]interface{}), desc.m_arrObj)
members, ok := objArr[i].([]interface{})
if !ok {
return nil, ECGO_INVALID_PARAMETER_VALUE.throw()
}
tdArr, err := sv.toStruct(members, desc.m_arrObj)
if err != nil {
return nil, err
}
Expand All @@ -161,14 +172,18 @@ func (sv TypeData) toArray(objArr []interface{}, desc *TypeDescriptor) ([]TypeDa
tmp, ok := objArr[i].([]interface{})

if !ok && desc.m_arrObj.m_arrObj != nil {
obj, err := sv.makeupObjToArr(tmp[i], desc.m_arrObj)
obj, err := sv.makeupObjToArr(objArr[i], desc.m_arrObj)
if err != nil {
return nil, err
}
objArr[i] = obj
tmp = obj
}
if tmp == nil {
return nil, ECGO_INVALID_PARAMETER_VALUE.throw()
}

tdArr, err := sv.toArray(objArr[i].([]interface{}), desc.m_arrObj)
tdArr, err := sv.toArray(tmp, desc.m_arrObj)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading