From 3723610d07fc02f15cf6f686805e785df545bff5 Mon Sep 17 00:00:00 2001 From: he_sk Date: Sun, 27 Sep 2026 01:49:22 +0800 Subject: [PATCH 1/2] Support nested object and array values without crashes --- .../2026-09-26-type-connection-matrix.md | 4 + .../test-results/2026-09-27-complex-arrays.md | 18 +++ dpi_bridge/dpi_obj.go | 40 ++++- .../third_party/chunanyong_dm/PATCHES.md | 18 +++ .../chunanyong_dm/complex_array_test.go | 18 +++ .../chunanyong_dm/security/zzi_test.go | 137 ++++++++++++++++++ .../third_party/chunanyong_dm/type_info.go | 12 ++ dpi_bridge/third_party/chunanyong_dm/zzo.go | 27 +++- tests/integration/test_p1_array_types.py | 74 ++++++++++ tests/integration/test_p1_object_types.py | 36 +++++ 10 files changed, 370 insertions(+), 14 deletions(-) create mode 100644 docs/test-results/2026-09-27-complex-arrays.md create mode 100644 dpi_bridge/third_party/chunanyong_dm/complex_array_test.go create mode 100644 dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go diff --git a/docs/test-results/2026-09-26-type-connection-matrix.md b/docs/test-results/2026-09-26-type-connection-matrix.md index 7bf4262..c51689d 100644 --- a/docs/test-results/2026-09-26-type-connection-matrix.md +++ b/docs/test-results/2026-09-26-type-connection-matrix.md @@ -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 驱动对嵌套数组指针输入发生进程崩溃的问题。 diff --git a/docs/test-results/2026-09-27-complex-arrays.md b/docs/test-results/2026-09-27-complex-arrays.md new file mode 100644 index 0000000..37b2659 --- /dev/null +++ b/docs/test-results/2026-09-27-complex-arrays.md @@ -0,0 +1,18 @@ +# 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. + +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. diff --git a/dpi_bridge/dpi_obj.go b/dpi_bridge/dpi_obj.go index 9643eef..60ef573 100644 --- a/dpi_bridge/dpi_obj.go +++ b/dpi_bridge/dpi_obj.go @@ -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) } @@ -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") } @@ -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} @@ -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) diff --git a/dpi_bridge/third_party/chunanyong_dm/PATCHES.md b/dpi_bridge/third_party/chunanyong_dm/PATCHES.md index 3c47bc6..aebdad6 100644 --- a/dpi_bridge/third_party/chunanyong_dm/PATCHES.md +++ b/dpi_bridge/third_party/chunanyong_dm/PATCHES.md @@ -115,6 +115,24 @@ - 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. +- 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` diff --git a/dpi_bridge/third_party/chunanyong_dm/complex_array_test.go b/dpi_bridge/third_party/chunanyong_dm/complex_array_test.go new file mode 100644 index 0000000..932f517 --- /dev/null +++ b/dpi_bridge/third_party/chunanyong_dm/complex_array_test.go @@ -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) + } + } +} diff --git a/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go b/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go new file mode 100644 index 0000000..43b6c09 --- /dev/null +++ b/dpi_bridge/third_party/chunanyong_dm/security/zzi_test.go @@ -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) + } +} diff --git a/dpi_bridge/third_party/chunanyong_dm/type_info.go b/dpi_bridge/third_party/chunanyong_dm/type_info.go index 7b8b616..30834e3 100644 --- a/dpi_bridge/third_party/chunanyong_dm/type_info.go +++ b/dpi_bridge/third_party/chunanyong_dm/type_info.go @@ -5,6 +5,8 @@ package dm type ArrayTypeInfo struct { Kind int ElementKind int + ElementTypeName string + ElementTypeOID int ElementPrecision int ElementScale int MaxElements int @@ -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, diff --git a/dpi_bridge/third_party/chunanyong_dm/zzo.go b/dpi_bridge/third_party/chunanyong_dm/zzo.go index 77bccc9..6ce0faf 100644 --- a/dpi_bridge/third_party/chunanyong_dm/zzo.go +++ b/dpi_bridge/third_party/chunanyong_dm/zzo.go @@ -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 } @@ -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 } diff --git a/tests/integration/test_p1_array_types.py b/tests/integration/test_p1_array_types.py index 8e4dc2e..ba025c5 100644 --- a/tests/integration/test_p1_array_types.py +++ b/tests/integration/test_p1_array_types.py @@ -114,3 +114,77 @@ def test_object_with_varray_keeps_decimal_elements(conn, table_name_factory, dro else: cleanup.execute(f"DROP TYPE {name}") conn.commit() + + +def test_varray_of_objects_preserves_null_members(conn, table_name_factory, drop_table): + object_type = table_name_factory("DMPY_ARRAY_ITEM") + array_type = table_name_factory("DMPY_OBJECT_ARRAY") + table = table_name_factory("DMPY_OBJECT_ARRAY_TAB") + values = [ + [Decimal("12345678901234567890.12345678"), "汉字"], + None, + [None, "emoji 😀"], + ] + cur = conn.cursor() + created = [] + try: + cur.execute(f"CREATE TYPE {object_type} AS OBJECT (AMOUNT DECIMAL(30,8), LABEL VARCHAR(20))") + created.append(("TYPE", object_type)) + cur.execute(f"CREATE TYPE {array_type} AS VARRAY(3) OF {object_type}") + created.append(("TYPE", array_type)) + cur.execute(f"CREATE TABLE {table} (V {array_type})") + created.append(("TABLE", table)) + conn.commit() + + value = dmPython.objectvar(conn, array_type) + value.setvalue(values) + cur.execute(f"INSERT INTO {table} VALUES (?)", (value,)) + conn.commit() + + cur.execute(f"SELECT V FROM {table}") + assert cur.fetchone()[0].getvalue() == values + finally: + conn.rollback() + with conn.cursor() as cleanup: + for kind, name in reversed(created): + if kind == "TABLE": + drop_table(cleanup, name) + else: + cleanup.execute(f"DROP TYPE {name}") + conn.commit() + cur.close() + + +def test_varray_of_varray_keeps_decimal_elements(conn, table_name_factory, drop_table): + inner_type = table_name_factory("DMPY_NEST_INNER") + outer_type = table_name_factory("DMPY_NEST_OUTER") + table = table_name_factory("DMPY_NEST_ARRAY_TAB") + values = [[Decimal("12345678901234567890.12345678"), None], [Decimal("0.00000001")]] + cur = conn.cursor() + created = [] + try: + cur.execute(f"CREATE TYPE {inner_type} AS VARRAY(2) OF DECIMAL(30,8)") + created.append(("TYPE", inner_type)) + cur.execute(f"CREATE TYPE {outer_type} AS VARRAY(2) OF {inner_type}") + created.append(("TYPE", outer_type)) + cur.execute(f"CREATE TABLE {table} (V {outer_type})") + created.append(("TABLE", table)) + conn.commit() + + value = dmPython.objectvar(conn, outer_type) + value.setvalue(values) + cur.execute(f"INSERT INTO {table} VALUES (?)", (value,)) + conn.commit() + + cur.execute(f"SELECT V FROM {table}") + assert cur.fetchone()[0].getvalue() == values + finally: + conn.rollback() + with conn.cursor() as cleanup: + for kind, name in reversed(created): + if kind == "TABLE": + drop_table(cleanup, name) + else: + cleanup.execute(f"DROP TYPE {name}") + conn.commit() + cur.close() diff --git a/tests/integration/test_p1_object_types.py b/tests/integration/test_p1_object_types.py index fda6681..9ead329 100644 --- a/tests/integration/test_p1_object_types.py +++ b/tests/integration/test_p1_object_types.py @@ -99,3 +99,39 @@ def test_unknown_object_type_reports_error(conn, table_name_factory): type_name = table_name_factory("DMPY_MISSING") with pytest.raises(dmPython.DatabaseError, match="not visible"): dmPython.objectvar(conn, type_name) + + +def test_nested_object_null_members_roundtrip(conn, table_name_factory, drop_table): + child = table_name_factory("DMPY_NULL_CHILD") + parent = table_name_factory("DMPY_NULL_PARENT") + table = table_name_factory("DMPY_NULL_OBJECT_TAB") + cases = [[1, None], [2, [None, None]], [3, [Decimal("0.00000001"), None]]] + cur = conn.cursor() + created = [] + try: + cur.execute(f"CREATE TYPE {child} AS OBJECT (AMOUNT DECIMAL(30,8), LABEL VARCHAR(20))") + created.append(("TYPE", child)) + cur.execute(f"CREATE TYPE {parent} AS OBJECT (ID INTEGER, ITEM {child})") + created.append(("TYPE", parent)) + cur.execute(f"CREATE TABLE {table} (V {parent})") + created.append(("TABLE", table)) + conn.commit() + + for fields in cases: + value = dmPython.objectvar(conn, parent) + value.setvalue(fields) + cur.execute(f"INSERT INTO {table} VALUES (?)", (value,)) + conn.commit() + + cur.execute(f"SELECT V FROM {table} ORDER BY V.ID") + assert [row[0].getvalue() for row in cur.fetchall()] == cases + finally: + conn.rollback() + with conn.cursor() as cleanup: + for kind, name in reversed(created): + if kind == "TABLE": + drop_table(cleanup, name) + else: + cleanup.execute(f"DROP TYPE {name}") + conn.commit() + cur.close() From 2da5480316441c40bc706a9acae9e9dd4a9e35b4 Mon Sep 17 00:00:00 2001 From: he_sk Date: Sun, 27 Sep 2026 01:53:32 +0800 Subject: [PATCH 2/2] Validate nested fetch handles before reuse --- docs/test-results/2026-09-27-complex-arrays.md | 5 +++++ dpi_bridge/dpi_fetch.go | 3 ++- dpi_bridge/third_party/chunanyong_dm/PATCHES.md | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/test-results/2026-09-27-complex-arrays.md b/docs/test-results/2026-09-27-complex-arrays.md index 37b2659..ae6a001 100644 --- a/docs/test-results/2026-09-27-complex-arrays.md +++ b/docs/test-results/2026-09-27-complex-arrays.md @@ -5,6 +5,11 @@ 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. diff --git a/dpi_bridge/dpi_fetch.go b/dpi_bridge/dpi_fetch.go index 5818974..0add205 100644 --- a/dpi_bridge/dpi_fetch.go +++ b/dpi_bridge/dpi_fetch.go @@ -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 diff --git a/dpi_bridge/third_party/chunanyong_dm/PATCHES.md b/dpi_bridge/third_party/chunanyong_dm/PATCHES.md index aebdad6..dc57b5c 100644 --- a/dpi_bridge/third_party/chunanyong_dm/PATCHES.md +++ b/dpi_bridge/third_party/chunanyong_dm/PATCHES.md @@ -121,6 +121,9 @@ - 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