Compatibility

Does Obfuscation Break Database Rows and ORM Models?

A row object is one of the few things in a JavaScript program that nobody in the program wrote. You send text to a database, the driver reads the column names out of the result, and an object appears with properties you never declared. That makes it a contract with something outside the build, and contracts with things outside the build are where member renaming stops being a formatting choice.

What was measured, against a real driver

One sample file driven through five protection profiles for the base column, and through both member-renaming profiles for everything after that. It uses node's built-in SQLite driver rather than a stand-in: it creates a table, seeds it, reads rows back with a prepared statement, does the arithmetic a line-items page does, runs a required-column guard, maps rows into a domain model, reads an aggregate with a SQL alias, and finishes with a lookup bound by named parameter.

Nothing in the sample authors the row keys. The SQL is text, the driver parses that text, and the properties on the returned object are created at run time from the schema. That is the whole basis of the measurement. A file that builds its own record shape and then reads it back proves only that renaming is internally consistent, which was never in question. Here the names cross a real boundary: a database wrote them, and this code reads them.

The base column is clean. On the default target, the modern target, the gate profile, the modern gate profile and the string-transform profile, the sample reproduced the unprotected output line for line: the driver key list, every column read, the order total, the guard verdict, the serialised row, the aggregate and the parameter-bound lookup. If you protect with a default configuration, your data layer is not a surface you need to think about.

Two arms are reported as inconclusive rather than as passes. Patterns matching the primary key and the currency column renamed nothing at all in this sample, because neither name is ever read as a property there. An arm that renames nothing measures nothing, and calling it a pass would be the most flattering way to be wrong.

Every column read comes back undefined

With member renaming on and a pattern matching the product code column, the first row's product code went from the real value to undefined. The emitted file asks the row for a generated name; the driver put the original name there; the property does not exist, so the read produces undefined with nothing thrown.

The list of keys the driver actually built was unchanged in every arm. That is worth stating plainly, because it is the fastest way to see the shape of the problem: the row carries the right names and the code asks for different ones. A log line that prints the row wholesale looks perfect. The line beside it that reads one field prints nothing useful.

The arithmetic arm is the one that reaches a customer. With a pattern matching the quantity and unit-price columns, both reads returned undefined and the order total went from a correct figure to NaN. There is no error at any point. A total of NaN renders as the text NaN in most templates, is stored happily by any column typed as text, and is rejected much later by something that expected a number.

Aggregates behave the same way and are easier to miss, because the alias feels like your own name. A statement selecting a count and a sum under aliases returns an object keyed by exactly those aliases, and both reads came back undefined under a pattern that matched them. The alias you typed is inside a string; the property you read is in code. Only one of the two is a rename site.

The guard that was written to catch this reports nothing wrong

The sample carries a required-column check of the kind people add after being burned once: a list of column names, and a test that each one is present on the row. Its verdict never moved. It printed that no columns were missing in every arm, including the arms where every read was broken.

The mechanism is the asymmetry this site keeps arriving at from different directions. The list of required columns is a list of strings, and a string is not an identifier. The row's keys came from the driver, so they are not renamed either. The check compares one unrenamed thing against another and correctly reports agreement. The renamed half is the code that reads the values, which the check never touches.

That makes the guard a false comfort rather than a defence, and it is the reason this failure class survives a careful review. The startup check passes, the schema is right, the query is right, and the values are wrong. If you want a check that fails when this happens, compare a value rather than a key: read one column, compare it against a constant the database wrote, and let the assertion carry the value in its message.

Columns disappear from the JSON you send onward

Two arms show the outbound half, and it does not look like a renaming problem when it arrives. Serialising a row after renaming the product code produced a document with that field simply absent. Serialising after renaming quantity and unit price left a document with one field in it. The values were not wrong; the fields were gone.

This follows from an ordinary rule that has nothing to do with protection. A renamed read yields undefined, and JSON.stringify omits any property whose value is undefined. So the field is not transmitted as a generated name, which at least would be visible in a network trace. It is not transmitted at all, and the receiving system reports a required property missing on a field your source demonstrably sets.

The mapping layer most codebases put between rows and their domain model sits on the other side of that line. When the pattern matched the model's own field names -- the ones the mapping declares, not the column names -- the values stayed correct and the emitted document carried generated keys instead. Both halves of that read live in your source, so they were renamed together and the program is internally consistent. Only the document that leaves the process moved, which is the quietest place for it to move.

Named parameters are the loud half, and they name the generated identifier

The one arm that fails immediately is the one where your object is handed to the driver rather than read from it. A lookup bound by named parameter went from returning a row to throwing, with the driver's own message: unknown named parameter, quoting the generated name.

That is a good failure. The build produces an object whose keys the driver has never seen, the driver refuses it, and the error names the thing that changed. Compare it with every other arm on this page, where nothing is thrown and a wrong value continues down the call stack. If your data access uses named parameters throughout, a renamed column pattern is likely to stop your application at the first query rather than corrupt a report a week later.

The direction of the contract is what decides which of those two you get. When the driver writes the keys and you read them, renaming is silent. When you write the keys and the driver reads them, renaming is loud. It is the same option, the same pattern and the same column name; only the side of the boundary changed.

What this means for an ORM, and how to check your own build

An ORM does not change the mechanics, it just moves them. The model class you declare is your source and is a rename site. The rows the driver returns are not. Every ORM sits astride that line: the column-to-property mapping table, the attribute list, the select projection you write as an array of strings, and the instance properties your application reads. Some of those are text and survive; some are identifiers and do not.

The practical rule is the same one that governs every other external contract on this site. Scope the member pattern to names you own end to end, and keep it away from anything a database, a driver or a migration wrote. A naming convention makes that easy to enforce: mark private members with a prefix, and let the pattern match only that prefix, so a column name can never match by accident.

To check a build you already have, do not compare a schema against a key list, because both sides of that comparison survive renaming. Read one row, print a value the database wrote, and compare it to a literal you typed. Then print the serialised row and count its fields. Two lines of output will tell you whether the layer is intact, and neither of them can pass by comparing one renamed read against another.

Frequently asked questions

Does obfuscation break database access in JavaScript?

Not in the default configuration. A sample driving node's built-in SQLite driver -- creating a table, reading rows through a prepared statement, computing a total, mapping to a model and binding a named parameter -- produced identical output on all five protection profiles measured. A data layer only becomes a surface when member renaming is switched on and the pattern matches column names.

Why does a column read as undefined after obfuscation?

Because the driver built the row from your SQL text and your code now asks for a different name. Column names arrive at run time from the schema, which is not a rename site, so the emitted file reads a generated identifier that the row does not carry. In the measured run the value came back undefined and nothing was thrown.

Why did my order total become NaN after protecting the build?

Because two renamed column reads returned undefined and arithmetic on undefined produces NaN. The measured sample went from a correct order total to NaN with no error at any point. NaN renders as text in most templates and is accepted by any column typed as text, so it is usually discovered downstream rather than where it happened.

Why does my required-column check pass while every value is wrong?

Because both sides of that check survive renaming. The required list is a list of strings and the row keys were written by the driver, so the comparison is between two unrenamed things and correctly reports agreement. Only the code that reads the values was renamed. Compare a value against a constant instead, and let the assertion print it.

Do columns go missing from the JSON my API returns?

Yes, and that is how this usually surfaces. A renamed read yields undefined, and JSON.stringify omits undefined-valued properties, so the field is absent rather than present under a generated name. The receiving system reports a required property missing on a field your source plainly sets, which sends the investigation in the wrong direction.

What happens to named query parameters under member renaming?

They fail immediately and helpfully. Handing the driver an object whose keys have been renamed produced an error naming the unknown parameter, quoting the generated identifier. That is the loud half of this class: when you write the keys and the driver reads them the failure is instant, and when the driver writes them and you read them it is silent.

How should I scope a member pattern in a project with a database?

Anchor it to names you own on both ends, and keep it clear of anything a schema, a driver or a migration authored. A prefix convention for private members is the cheapest enforcement, because a column name can then never match by accident. The member renaming documentation sets out the same workflow, including keeping every related file in one project.

Related reading