Skip to content

Commit 06ba07e

Browse files
dhensbyCopilot
andcommitted
fix: include schema in TVP type declaration for sp_executesql
Fixes #1759. When using TVPs with schema-qualified UDT names (e.g. AI.UDT_StringArray), the schema was omitted from the @params string in sp_executesql, causing 'Cannot find data type' errors. Root cause: tedious's TVP declaration function only uses value.name, ignoring value.schema. This creates a SchemaAwareTVP type that inherits all behavior from tedious's TVP type but overrides declaration to include the schema when present. The fix does not affect the execute() path (stored procedures via RPC protocol) which already handles schema correctly via generateTypeInfo. Only the query() path using sp_executesql was affected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2f39ff5 commit 06ba07e

2 files changed

Lines changed: 155 additions & 1 deletion

File tree

lib/tedious/request.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ const N_TYPES = {
2323
NumericN: 0x6C
2424
}
2525

26+
// Workaround for tedious TVP declaration not including schema name.
27+
// See: https://github.com/tediousjs/node-mssql/issues/1759
28+
const SchemaAwareTVP = Object.create(tds.TYPES.TVP, {
29+
declaration: {
30+
value: function (parameter) {
31+
const value = parameter.value
32+
if (value && value.schema) {
33+
return value.schema + '.' + value.name + ' readonly'
34+
}
35+
return value.name + ' readonly'
36+
},
37+
writable: true,
38+
configurable: true
39+
}
40+
})
41+
2642
const getTediousType = function (type) {
2743
switch (type) {
2844
case TYPES.VarChar: return tds.TYPES.VarChar
@@ -54,7 +70,7 @@ const getTediousType = function (type) {
5470
case TYPES.Binary: return tds.TYPES.Binary
5571
case TYPES.VarBinary: return tds.TYPES.VarBinary
5672
case TYPES.UDT: case TYPES.Geography: case TYPES.Geometry: return tds.TYPES.UDT
57-
case TYPES.TVP: return tds.TYPES.TVP
73+
case TYPES.TVP: return SchemaAwareTVP
5874
case TYPES.Variant: return tds.TYPES.Variant
5975
default: return type
6076
}

test/common/unit.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,4 +724,142 @@ describe('connection string auth - tedious', () => {
724724
}
725725
})
726726
})
727+
728+
describe('TVP declaration with schema', () => {
729+
const tds = require('tedious')
730+
731+
it('documents upstream tedious bug: schema missing from TVP declaration', () => {
732+
const tvp = new sql.Table('AI.UDT_StringArray')
733+
tvp.columns.add('Name', sql.NVarChar(128), { nullable: false })
734+
tvp.rows.add('TestValue1')
735+
736+
const tvpValue = {
737+
name: tvp.name,
738+
schema: tvp.schema,
739+
columns: [],
740+
rows: tvp.rows
741+
}
742+
743+
// tedious's own TVP type does NOT include schema in declaration
744+
const req = new tds.Request('SELECT 1')
745+
req.addParameter('InputList', tds.TYPES.TVP, tvpValue)
746+
const paramsStr = req.makeParamsParameter(req.parameters)
747+
assert.strictEqual(paramsStr, '@InputList UDT_StringArray readonly',
748+
'tedious itself does not include schema - this documents the upstream bug')
749+
})
750+
751+
it('schema-aware TVP type includes schema in declaration', () => {
752+
const tvp = new sql.Table('AI.UDT_StringArray')
753+
tvp.columns.add('Name', sql.NVarChar(128), { nullable: false })
754+
tvp.rows.add('TestValue1')
755+
756+
const tvpValue = {
757+
name: tvp.name,
758+
schema: tvp.schema,
759+
columns: [],
760+
rows: tvp.rows
761+
}
762+
763+
assert.strictEqual(tvp.name, 'UDT_StringArray')
764+
assert.strictEqual(tvp.schema, 'AI')
765+
766+
// Use the patched SchemaAwareTVP type from lib/tedious/request.js
767+
// Since it's not exported, recreate the same pattern to verify behavior
768+
const SchemaAwareTVP = Object.create(tds.TYPES.TVP, {
769+
declaration: {
770+
value: function (parameter) {
771+
const value = parameter.value
772+
if (value && value.schema) {
773+
return value.schema + '.' + value.name + ' readonly'
774+
}
775+
return value.name + ' readonly'
776+
},
777+
writable: true,
778+
configurable: true
779+
}
780+
})
781+
782+
const req = new tds.Request('SELECT 1')
783+
req.addParameter('InputList', SchemaAwareTVP, tvpValue)
784+
const paramsStr = req.makeParamsParameter(req.parameters)
785+
assert.strictEqual(paramsStr, '@InputList AI.UDT_StringArray readonly')
786+
})
787+
788+
it('schema-aware TVP works without schema', () => {
789+
const tvp = new sql.Table('UDT_StringArray')
790+
tvp.columns.add('Name', sql.NVarChar(128), { nullable: false })
791+
792+
const tvpValue = {
793+
name: tvp.name,
794+
schema: tvp.schema,
795+
columns: [],
796+
rows: tvp.rows
797+
}
798+
799+
assert.strictEqual(tvp.name, 'UDT_StringArray')
800+
assert.strictEqual(tvp.schema, null)
801+
802+
const SchemaAwareTVP = Object.create(tds.TYPES.TVP, {
803+
declaration: {
804+
value: function (parameter) {
805+
const value = parameter.value
806+
if (value && value.schema) {
807+
return value.schema + '.' + value.name + ' readonly'
808+
}
809+
return value.name + ' readonly'
810+
},
811+
writable: true,
812+
configurable: true
813+
}
814+
})
815+
816+
const req = new tds.Request('SELECT 1')
817+
req.addParameter('InputList', SchemaAwareTVP, tvpValue)
818+
const paramsStr = req.makeParamsParameter(req.parameters)
819+
assert.strictEqual(paramsStr, '@InputList UDT_StringArray readonly')
820+
})
821+
822+
it('schema-aware TVP inherits generateTypeInfo from tedious TVP', () => {
823+
const SchemaAwareTVP = Object.create(tds.TYPES.TVP, {
824+
declaration: {
825+
value: function (parameter) {
826+
const value = parameter.value
827+
if (value && value.schema) {
828+
return value.schema + '.' + value.name + ' readonly'
829+
}
830+
return value.name + ' readonly'
831+
},
832+
writable: true,
833+
configurable: true
834+
}
835+
})
836+
837+
// Verify it inherits all other methods from tedious TVP
838+
assert.strictEqual(SchemaAwareTVP.id, tds.TYPES.TVP.id)
839+
assert.strictEqual(SchemaAwareTVP.type, tds.TYPES.TVP.type)
840+
assert.strictEqual(SchemaAwareTVP.name, tds.TYPES.TVP.name)
841+
assert.strictEqual(SchemaAwareTVP.generateTypeInfo, tds.TYPES.TVP.generateTypeInfo)
842+
assert.strictEqual(SchemaAwareTVP.generateParameterLength, tds.TYPES.TVP.generateParameterLength)
843+
assert.strictEqual(SchemaAwareTVP.generateParameterData, tds.TYPES.TVP.generateParameterData)
844+
assert.strictEqual(SchemaAwareTVP.validate, tds.TYPES.TVP.validate)
845+
// declaration is overridden
846+
assert.notStrictEqual(SchemaAwareTVP.declaration, tds.TYPES.TVP.declaration)
847+
})
848+
})
849+
850+
describe('msnodesqlv8 TVP declaration', () => {
851+
const { declare, TYPES } = require('../../lib/datatypes')
852+
853+
it('includes schema in TVP declaration when tvpType is schema-qualified', () => {
854+
// msnodesqlv8 uses declare() from datatypes.js, which uses tvpType
855+
// When tvpType includes the schema, the declaration is correct
856+
const result = declare(TYPES.TVP, { tvpType: 'AI.UDT_StringArray' })
857+
assert.strictEqual(result, 'AI.UDT_StringArray readonly')
858+
})
859+
860+
it('works without schema in tvpType', () => {
861+
const result = declare(TYPES.TVP, { tvpType: 'UDT_StringArray' })
862+
assert.strictEqual(result, 'UDT_StringArray readonly')
863+
})
864+
})
727865
})

0 commit comments

Comments
 (0)