Skip to content

[Bug] add-data-source discards the SQL schema qualifier from a table's identity — two same-named tables in different schemas silently overwrite each other #411

Description

@edbellman

Describe the bug

add-data-source derives a tabular data source's identity (its alias in power.config.json, its schema file name, and its generated model/service names) from the leaf name only — the segment after the final dot. The [schema]. qualifier is stripped and never recovered.

Consequently, two different tables that share a base name in different schemas of the same database — [sales].[Contacts] and [ops].[Contacts] — both resolve to the alias contacts, and there is no --name / --alias parameter to disambiguate them.

The result is not an error. The second add:

  1. Overwrites .power/schemas/sql/<dataset>/contacts.Schema.json with the second table's metadata, and regenerates ContactsModel / ContactsService from it;
  2. Leaves power.config.json routing the contacts alias at the first table, because the dataset update is guarded by if (!dataSources.includes(alias)), which is already true.

So the app ships a service typed from [ops].[Contacts] but wired to [sales].[Contacts]. It compiles, it runs, and it returns the wrong rows. Nothing surfaces until someone notices the data is wrong.

This is distinct from the already-understood same-name-across-two-connections case. This is a single connection, a single dataset, two schemas.

Steps to Reproduce

CREATE SCHEMA sales;
CREATE SCHEMA ops;
CREATE TABLE sales.Contacts (Id int IDENTITY PRIMARY KEY, SalesOnlyColumn nvarchar(50));
CREATE TABLE ops.Contacts   (Id int IDENTITY PRIMARY KEY, OpsOnlyColumn   nvarchar(50));
pac code add-data-source -a "shared_sql" -c <connectionId> `
  -d "myserver.database.windows.net,mydb" -t "[sales].[Contacts]"

pac code add-data-source -a "shared_sql" -c <connectionId> `
  -d "myserver.database.windows.net,mydb" -t "[ops].[Contacts]"

Both commands report success. Then inspect:

  • .power/schemas/sql/myserver.database.windows.net,mydb/ — only one contacts.Schema.json, holding ops.Contacts.
  • src/generated/models/ContactsModel.ts — exposes OpsOnlyColumn; SalesOnlyColumn is gone.
  • power.config.json — one alias, still pointing at the first table:
"dataSets": {
  "myserver.database.windows.net,mydb": {
    "dataSources": {
      "contacts": { "tableName": "[sales].[Contacts]" }
    }
  }
}

await ContactsService.getAll() now returns sales.Contacts rows against an ops.Contacts type.

Expected behavior

One of:

  1. Preferred — include the schema qualifier in the identity so the two tables are distinguishable (e.g. aliases sales_contacts / ops_contacts, or feed the qualifier into the existing _1 disambiguation so the second becomes contacts_1); or
  2. Add an explicit --name / --alias parameter so the caller can name the data source; or
  3. At minimum, fail loudly — refuse the second add with "a data source named contacts already exists and points at [sales].[Contacts]" rather than producing a mismatched type/route pair.

Actual behavior

Both adds succeed. The generated code is typed from the second table, the routing points at the first, and the first table becomes unreachable from the app.

Root cause

Traced through the code-gen library. Paths below are relative to @microsoft/power-apps-actions/dist.

In 1.8.0 (current npm CLI, @microsoft/power-apps-cli@0.13.0):

  • CodeGen/shared/nameUtils.js:160sanitizeTabularResourceName() does result.match(/\.?([^.]*)$/), i.e. keeps only the segment after the final dot. [sales].[Contacts]Contacts.
  • Actions/AddDataSource.js:386 — that value becomes tabularMetadata.friendlyName.
  • ReferenceStores/connectionReferences.js:36friendlyName becomes desiredName.
  • ReferenceStores/connectionReferences.js:191-193getExistingCdpNames() excludes every alias registered in the same dataset on the same reference, so getSanitizedUniqueName() never sees the existing contacts and never allocates contacts_1. That exclusion is intentional (it keeps re-adding the same table idempotent), but idempotency is keyed on the alias, and the alias no longer carries enough information to tell two tables apart.
  • ReferenceStores/connectionReferences.js:58 — the if (!dataSources.includes(dataSourceName)) guard then skips the dataSets update, so the routing keeps the first table.
  • Actions/AddDataSource.js saveMetadata() — writes ${dsName}.Schema.json unconditionally, overwriting the first schema file.

Secondary, pac-only: pac 2.9.3 bundles @microsoft/power-apps-actions 1.1.1, where the collision check is additionally case-sensitivenameUtils.js:107 does nameSet.has(desiredName) with case preserved, against stored aliases that were lowercased on write. So on pac, _1 disambiguation never fires for any mixed-case table name, including the genuinely-different-connection and different-database cases that 1.8.0 handles correctly. 1.1.1 codegen (CodeGen/shared/modelUtils.js:39-44) also has no suffix support at all, so there is no way to recover by hand. Anyone on pac code is on this older path.

Environment information

  • Framework / build tool: React + Vite
  • Connector: SQL Server (shared_sql), Azure SQL, cloud connection (no gateway)
  • pac CLI 2.9.3 → @microsoft/power-apps-actions 1.1.1
  • Also verified against @microsoft/power-apps-cli 0.13.0 → @microsoft/power-apps-actions 1.8.0
  • OS: macOS

Additional context

Non-dbo schemas are normal in any database that isn't greenfield — multi-tenant or multi-domain schemas routinely repeat entity names (sales.Contacts, ops.Contacts). Silent data mis-binding is the worst available failure mode here; even without the full fix, (3) fail loudly would convert this from a wrong-data defect into a five-minute diagnosis.

Related but distinct:

Current workaround: create a wrapper view or synonym so the leaf names differ (CREATE VIEW dbo.vw_Ops_Contacts AS SELECT * FROM ops.Contacts). That requires DDL rights on the source database, which isn't always available.


Disclaimer: I investigated and drafted this issue with Claude Code (Anthropic) — it read the bundled @microsoft/power-apps-actions source in both shipped CLI paths and traced the mechanism behind the collision I hit in my own project. I've reviewed and verified the report before filing.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions