Skip to main content

The public API, and where it stops

From 1.0, some of what SQLens exposes is a promise and the rest is implementation. This page draws that line explicitly, because a boundary implied by omission is not a boundary: the next person to depend on an internal class has no way to know they did.

It gathers what is already written elsewhere and links rather than restates it. Three contract documents that disagree are worse than one that is incomplete, so each promise has exactly one source and this page points at it.

The four promises

These are public API. Renaming or repurposing any of them is a breaking change.

PromiseWhat it coversWritten in full
Rule idsPG.L2.INDEX_NOT_CONCURRENT and its kindConventions
Message prefixesthe namespace every finding's identifier carries, one per suiteConventions
Exit codesthe four values and their meaningsThe public contracts
The baseline formatthe file a repository commits, including its schema versionThe public contracts

The message prefixes, in full:

sqlens.lint sqlens.audit sqlens.security sqlens.deploy sqlens.analyse

They are listed in a block rather than inline, and that is not formatting: a prefix written as an inline code span is indistinguishable from a CONFIG PATH — same shape, same leading sqlens. — and the guard that keeps this portal from citing configuration keys the schema does not declare reads every such span as one. The block says the same thing and cannot be misread by either a person or the guard.

A rule is never deleted. When one is superseded it becomes deprecated: it stops producing findings and says so, naming what replaced it, and a suppression that names it keeps working.

Also public, and easy to overlook

The four above are the ones a breaking change is measured against. Two more are public in the sense that something outside this package reads them:

  • The rule contract — the interfaces a third-party rule package implements. It is the extension point the ecosystem is built on, so changing its shape breaks packages this repository cannot see. There are two remediation seams among them, and they are not interchangeable: ProvidesRemediation rewrites a statement you wrote, and ProvidesSchemaObjectRemediation describes a migration for an object the run found in your database. The first takes a canonicalized statement, which is what guarantees its placeholders are filled from a form that cannot disagree with itself; a catalog rule has no statement, so it can never satisfy that seam. Widening the parameter to accept both would have made that guarantee conditional rather than adding a case to it — two acts, two contracts, each with the guarantee it can actually give.

    The set is enumerated, not implied — and here it is, because a page that points at a PHP constant has not named anything a reader of this site can see:

    InterfaceIts methodWhat implementing it says
    Ruleid(), category(), level(), …the central contract every rule fulfills. Everything else on this list is optional on top of it
    JudgesMigrationStatementsjudgeStatement()this rule can answer from a migration statement
    JudgesSchemaObjectsjudgeSchemaObject()this rule can answer from a live schema object — the other half of "one rule, two subjects"
    DeclaresJudgedObjectTypesjudgedObjectTypes()it judges only SOME kinds of object, and says which, so a run can tell whether it was ever handed one
    ChecksServerSettingsettingDriver(), settingVariable()it judges one server variable, and names the variable rather than leaving it to be inferred
    DeclaresSecurityPostureseverity(), limitations()what every security rule owes: its own severity axis, and what it cannot see
    DerivesDowntimeClassdowntimeClassFor()its downtime class is derived per statement rather than fixed for the rule
    ProvidesRemediationremediationFor()it can contribute a safe fix sequence for the statement it just judged
    ProvidesSchemaObjectRemediationremediationForObject()it can contribute fix material for the catalog object it just judged
    JudgesTheServerItRunsOnserverSubjectJudged()its subject is the SERVER or the role the audit connects as, not the schema — so a project that declared security.server.lifetime = disposable gets not_applicable from it rather than a failure about a container the job creates and destroys
    DeclaresConfigurationReachsilencedByConfiguration()its own configuration can leave it unable to report anything — a fifth state the level, category, stability and version gates cannot express, because they decide whether a rule is in the set at all rather than whether it can still answer

    ApiSurface::EXTENSION_CONTRACTS remains the machine-readable source, the published snapshot carries each one's method signatures, and the breaking-change check reads them. A signature that moves — a parameter type, a parameter name, a return type, a method added or removed — is forbidden without a major, in both directions. An interface has no benign direction: adding a method breaks every implementer, removing one breaks every caller.

    ⚠️ The second seam ships preview, like the payload it produces. That is a decision rather than an omission: it has no consumers yet, and the format it hands over is itself preview, so freezing the contract that produces it before anybody has built against either would guarantee freezing the wrong thing. It is promoted deliberately, and the enumeration above is what makes the promotion a visible act instead of a quiet one.

  • The JSON and SARIF report schema — including schema_version and the policy for raising it. See the JSON envelope and the schema-version policy.

The remediation payload is preview, and that is a promise too

The machine-readable fix material every safety finding can carry is not yet on the list above. It ships marked preview, and the marking is in the payload itself rather than only here — every payload carries its own schema_version and stability, so a consumer reading one never has to find this page to know what it may rely on.

What preview buys, in both directions:

  • For us: the shape may still move within a minor release. It is a young format, agents are the first consumers, and freezing it before anybody had built against it would have guaranteed freezing the wrong thing.
  • For you: pin the version you read. A payload announces its own, so code that checks it before trusting the fields around it keeps working across a bump — and code that does not will break on one.

When the shape settles it is promoted deliberately, and after that a version bump costs a major release like any other promised format. The two states are not a matter of intent: the machine- readable surface carries remediation_schema_stability, and the breaking-change check reads it to decide which rule applies to a version change.

Explicitly NOT public

Everything below the contracts is implementation, and it moves in any release:

  • Concrete classes under the contracts — a rule class, a reporter, a formatter. The interface is the promise; the class behind it is not.
  • The catalog readers — what they query, how they batch it, the shape of a reading. They exist to answer rules, and their signatures follow whatever the rules need.
  • Canonicalization internals — how a statement is normalized before it is compared. The result is stable enough for the determinism guarantee; the path to it is not.
  • Everything under Support, Engine and the driver namespaces.

Depending on any of these is allowed — it is ordinary PHP — but it is not covered, and a minor release may move it without a changelog entry.

Commands: the rule that decides

A command registered in the regular service provider appears in artisan list in every application that installs this package, and is therefore part of the promise. A developer command is registered exclusively through a development/test service provider, and is not.

That sentence is the whole rule, and it is written this way round on purpose: membership follows from where the command is registered, not from what it is called or who meant it for whom. A tool nobody intended as public still becomes public the moment a composer require puts it in somebody's artisan list.

The commands covered by the promise today:

sqlens:agent-rules sqlens:audit sqlens:baseline sqlens:doctor
sqlens:drift sqlens:format sqlens:lint sqlens:mcp
sqlens:postdeploy sqlens:predeploy sqlens:security

This list is held to the booted package by a test, in both directions: a command registered in the regular provider that is missing here fails, and a command named here that the package does not register fails too. The second direction caught its first error on the day it was written — this list originally carried a tenth name that appears in a source comment as a planned command and is registered nowhere.

The promise is checked, not trusted

A versioning policy that only exists in prose is a policy that drifts. A release gate diffs the rule contract against the last released one and refuses a raised severity whose changelog callout is missing — along with any lowered severity, any deleted rule id, and any change it cannot classify.

The one deliberate exception to "new behavior waits for a major": a security severity may be raised in a minor, with a callout in the changelog. Waiting for a major to say a finding is worse than first judged protects the release schedule at the expense of the person running it.

Stability tiers, the deprecation policy and the security exception are stated in full under stability tiers — they are quoted here rather than redefined.