fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(demo): make the docker demo datasources reachable - #74

Merged
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity
Aug 30, 2026
Merged

fix(demo): make the docker demo datasources reachable#74
nadeem4 merged 1 commit into
mainfrom
fix/docker-demo-connectivity

Conversation

@nadeem4

Copy link
Copy Markdown
Owner

Two bugs, both of which had to be fixed for the Docker demo to work at all

Bug 1 — ${env:...} in any non-password field broke adapter validation

DatasourceRegistry.resolved_connection() wrapped every resolved ${...}
placeholder in SecretStr, not just the passwords:

ifisinstance(value, str) andvalue.startswith("${") andvalue.endswith("}"):
secret_val=self.find_and_resolve_secret(value)
resolved_connection[key] =SecretStr(secret_val) # unconditional

The wrap could not be made selective at that point, because ConnectionConfig
carries no type information to work from — it declares a single field, type: str,
with extra: "allow". Everything else (host, port, user, password, driver)
arrives as an untyped extra. The real types live in each adapter's own model, e.g.
PostgresConnectionConfig: host: str, user: str, password: SecretStr. The
registry has no way to tell a password apart from a hostname.

So user: "${env:DEMO_REF_USER}" reached the adapter as a SecretStr and validation
blew up:

pydantic_core._pydantic_core.ValidationError: 3 validation errors for PostgresConnectionConfig
host
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
user
Input should be a valid string [type=string_type, input_value=SecretStr('**********'), input_type=SecretStr]
port
Input should be a valid integer [type=int_type, input_value=SecretStr('**********'), input_type=SecretStr]

Every DEMO_DOCKER_DATASOURCES entry uses ${env:...} for user, so the Docker
demo has never been able to register a datasource.

The fix is to stop wrapping. Pydantic already coerces a plain string into
SecretStr for fields declared that way, so returning the resolved plain string
gives user a str and password a SecretStr — both correct, with masking
preserved exactly where it is declared.

Password masking — one consequence, handled

resolved_connection has a single caller, which immediately .model_dump()s the
result into connection_args. Tracing where that goes turned up two places where
the plaintext password was previously masked by SecretStr's repr and would now
be exposed:

  1. DatasourceAPI.get_datasource_details() returns adapter.connection_args
    verbatim to callers. Now masked by name via mask_connection_args.
  2. A pydantic ValidationError renders the whole input dict when a required
    field is missing, and nl2sql doctor prints that message straight to the
    console. Adapter construction is now wrapped so the message is redacted, and
    re-raised from None so the traceback does not carry the cause either.

The adapter itself still receives the real value — only the rendered views are
masked. The connection URI has always contained the plaintext password; that is
unchanged by this PR.

Bug 2 — localhost is the wrong host inside the app container

DEMO_DOCKER_DATASOURCES hardcoded host: "localhost" with the published ports
(5433, 5434, 3307, 1434). That is right from the developer's machine and wrong from
inside the app container, which has to reach the databases by Compose service name
on their internal ports.

Both callers have to keep working, so host and port are now env-driven with
host-appropriate defaults:

CallerHostPortSource
nl2sql --env demo index / runlocalhostpublished (5433/5434/3307/1434).env.demo
the app containerCompose service nameinternal (5432/5432/3306/1433)environment: on app, which overrides env_file

This uses the same ${env:...} convention already in place for user and password —
which is exactly why Bug 1 had to be fixed first, or the new host and port
references would have hit the same SecretStr failure.

Tests

New tests in the existing files:

  • resolved_connection returns str for non-secret fields, and a real
    PostgresConnectionConfig still receives a SecretStr password.
  • A DEMO_DOCKER_DATASOURCES-shaped config with ${env:...} in user, host
    and port registers and produces the expected URI. This fails against main
    with the three-error ValidationError quoted above.
  • Registration errors do not echo the resolved password; get_datasource_details
    masks it.
  • The generated Compose app service sets the service-name/internal-port
    overrides, the generated .env defaults to localhost + published ports, and
    the databases keep their published ports for host access.

Verification

  • Unit: 255 passed, 1 skipped, 47 deselected (was 247/1/47; +8 new), run twice.
  • Key-free integration: 28 passed, run twice.
  • nl2sql setup --demo --docker, then docker compose config --services:
    • default → manufacturing_ref, manufacturing_supply, manufacturing_ops, app
    • --profile mssql → the above plus manufacturing_history
    • Compose's own resolved output confirms the app service ends up with
      DEMO_REF_HOST: manufacturing_ref / DEMO_REF_PORT: "5432", overriding the
      localhost:5433 in .env.demo.
  • End-to-end without Docker: with the DEMO_* vars set, all four docker-shaped
    datasources resolve to plain str and build sane URIs, e.g.
    postgresql://ref_admin:********@manufacturing_ref:5432/manufacturing_ref and
    mssql+pyodbc://history_admin:********@manufacturing_history:1433/manufacturing_history?driver=ODBC+Driver+17+for+SQL+Server.

No images were pulled and no containers were started. Docs updated in
docs/getting_started/demo.md.

@nadeem4
nadeem4 merged commit b7edaf9 into mainAug 30, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nadeem4