From c0df89679f6e2779b51aac004b8b164a2f47dd30 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 1 May 2026 18:26:55 -0600 Subject: [PATCH] fix: reject legacy v4 'schema' field in DDL operations with 400 error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In v5, 'database' replaced 'schema' as the field name for database identification in DDL operations. Callers using the v4 'schema' field received a 200 success response but the operation silently failed cluster-wide — a foot-gun with no diagnostic signal. Adds rejectLegacySchemaField() called after Joi validation and before transformReq() in all DDL functions (dropTable, dropSchema, createTable, createSchemaStructure, dropAttribute, createAttribute). Internal replicated calls are unaffected because transformReq() on the originating node sets both fields before forwarding to peer nodes. Co-Authored-By: Claude Sonnet 4.6 --- dataLayer/schema.js | 16 +++++++--- unitTests/dataLayer/schema.test.js | 50 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/dataLayer/schema.js b/dataLayer/schema.js index 655a26a7c..04cc23297 100644 --- a/dataLayer/schema.js +++ b/dataLayer/schema.js @@ -18,6 +18,12 @@ const { transformReq } = require('../utility/common_utils.js'); const { server } = require('../server/Server.ts'); const { cleanupOrphans } = require('../resources/blob.ts'); +function rejectLegacySchemaField(obj) { + if (obj.schema && !obj.database) { + throw new ClientError("'database' is required. Use 'database' instead of the deprecated 'schema' field."); + } +} + const DB_NAME_CONSTRAINTS = Joi.string() .min(1) .max(commonValidators.schema_length.maximum) @@ -75,7 +81,7 @@ async function createSchemaStructure(schemaCreateObject) { }) ); if (validation) throw new ClientError(validation.message); - + rejectLegacySchemaField(schemaCreateObject); transformReq(schemaCreateObject); if (!(await schemaMetadataValidator.checkSchemaExists(schemaCreateObject.schema))) { @@ -95,6 +101,7 @@ async function createSchemaStructure(schemaCreateObject) { } async function createTable(createTableObject) { + rejectLegacySchemaField(createTableObject); transformReq(createTableObject); createTableObject.primary_key = createTableObject.primary_key ?? createTableObject.hash_attribute; return await createTableStructure(createTableObject); @@ -166,7 +173,7 @@ async function dropSchema(dropSchemaObject) { }) ); if (validation) throw new ClientError(validation.message); - + rejectLegacySchemaField(dropSchemaObject); transformReq(dropSchemaObject); let invalidSchemaMsg = await schemaMetadataValidator.checkSchemaExists(dropSchemaObject.schema); @@ -199,7 +206,7 @@ async function dropTable(dropTableObject) { }) ); if (validation) throw new ClientError(validation.message); - + rejectLegacySchemaField(dropTableObject); transformReq(dropTableObject); let invalidSchemaTableMsg = await schemaMetadataValidator.checkSchemaTableExists( @@ -242,7 +249,7 @@ async function dropAttribute(dropAttributeObject) { }) ); if (validation) throw new ClientError(validation.message); - + rejectLegacySchemaField(dropAttributeObject); transformReq(dropAttributeObject); let invalidSchemaTableMsg = await schemaMetadataValidator.checkSchemaTableExists( @@ -322,6 +329,7 @@ function dropAttributeFromGlobal(dropAttributeObject) { } async function createAttribute(createAttributeObject) { + rejectLegacySchemaField(createAttributeObject); transformReq(createAttributeObject); const tableAttr = getDatabases()[createAttributeObject.schema][createAttributeObject.table].attributes; diff --git a/unitTests/dataLayer/schema.test.js b/unitTests/dataLayer/schema.test.js index c4657a4d3..d3461bfef 100644 --- a/unitTests/dataLayer/schema.test.js +++ b/unitTests/dataLayer/schema.test.js @@ -638,3 +638,53 @@ describe.skip('Test schema module', function () { }); }); }); + +describe('Schema legacy field rejection', function () { + const LEGACY_ERROR_MSG = "Use 'database' instead of the deprecated 'schema' field"; + + async function expectLegacyRejection(fn, args) { + let error; + try { + await fn(...args); + } catch (err) { + error = err; + } + expect(error).to.be.instanceOf(Error); + expect(error.statusCode).to.equal(400); + expect(error.message).to.include(LEGACY_ERROR_MSG); + } + + it('rejects dropTable with schema-only field', () => + expectLegacyRejection(schema.dropTable, [{ operation: 'drop_table', schema: 'mydb', table: 'mytable' }])); + + it('rejects dropSchema with schema-only field', () => + expectLegacyRejection(schema.dropSchema, [{ operation: 'drop_schema', schema: 'mydb' }])); + + it('rejects createSchemaStructure with schema-only field', () => + expectLegacyRejection(schema.createSchemaStructure, [{ operation: 'create_schema', schema: 'mydb' }])); + + it('rejects createTable with schema-only field', () => + expectLegacyRejection(schema.createTable, [ + { operation: 'create_table', schema: 'mydb', table: 'mytable', primary_key: 'id' }, + ])); + + it('rejects dropAttribute with schema-only field', () => + expectLegacyRejection(schema.dropAttribute, [ + { operation: 'drop_attribute', schema: 'mydb', table: 'mytable', attribute: 'name' }, + ])); + + it('rejects createAttribute with schema-only field', () => + expectLegacyRejection(schema.createAttribute, [ + { operation: 'create_attribute', schema: 'mydb', table: 'mytable', attribute: 'name' }, + ])); + + it('does not reject dropTable when both database and schema are set', async function () { + let error; + try { + await schema.dropTable({ operation: 'drop_table', database: 'mydb', schema: 'mydb', table: 'mytable' }); + } catch (err) { + error = err; + } + expect(error?.message).to.not.include(LEGACY_ERROR_MSG); + }); +});