JDBC string support

This commit is contained in:
eelke 2025-08-31 10:22:08 +02:00
parent 1d53ca2fc2
commit 0090f39910
7 changed files with 396 additions and 28 deletions

View file

@ -14,6 +14,14 @@ public class ConnectionStringServiceTests
Assert.Equal(ConnStringFormat.Url, r.Value); Assert.Equal(ConnStringFormat.Url, r.Value);
} }
[Fact]
public void DetectFormat_Jdbc()
{
var r = svc.DetectFormat("jdbc:postgresql://localhost/db");
Assert.True(r.IsSuccess);
Assert.Equal(ConnStringFormat.Jdbc, r.Value);
}
[Fact] [Fact]
public void DetectFormat_Npgsql() public void DetectFormat_Npgsql()
{ {

View file

@ -0,0 +1,55 @@
using pgLabII.PgUtils.ConnectionStrings;
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
public class JdbcCodecTests
{
[Fact]
public void Parse_Basic()
{
var codec = new JdbcCodec();
var r = codec.TryParse("jdbc:postgresql://localhost:5433/mydb?sslmode=require&applicationName=app&connectTimeout=12");
Assert.True(r.IsSuccess);
var d = r.Value;
Assert.Single(d.Hosts);
Assert.Equal("localhost", d.Hosts[0].Host);
Assert.Equal((ushort)5433, d.Hosts[0].Port);
Assert.Equal("mydb", d.Database);
Assert.Equal(Npgsql.SslMode.Require, d.SslMode);
Assert.Equal("app", d.ApplicationName);
Assert.Equal(12, d.TimeoutSeconds);
}
[Fact]
public void Parse_MultiHost_MixedPorts()
{
var codec = new JdbcCodec();
var r = codec.TryParse("jdbc:postgresql://host1:5432,[::1]:5544,host3/db");
Assert.True(r.IsSuccess);
var d = r.Value;
Assert.Equal(3, d.Hosts.Count);
Assert.Equal("host1", d.Hosts[0].Host);
Assert.Equal((ushort)5432, d.Hosts[0].Port);
Assert.Equal("::1", d.Hosts[1].Host);
Assert.Equal((ushort)5544, d.Hosts[1].Port);
Assert.Equal("host3", d.Hosts[2].Host);
Assert.Null(d.Hosts[2].Port);
Assert.Equal("db", d.Database);
}
[Fact]
public void Format_RoundTrip()
{
var codec = new JdbcCodec();
var parsed = codec.TryParse("jdbc:postgresql://hostA,hostB:5555/test_db?applicationName=cli&param=x%20y");
Assert.True(parsed.IsSuccess);
var formatted = codec.TryFormat(parsed.Value);
Assert.True(formatted.IsSuccess);
var parsed2 = codec.TryParse(formatted.Value);
Assert.True(parsed2.IsSuccess);
Assert.Equal(parsed.Value.Hosts.Count, parsed2.Value.Hosts.Count);
Assert.Equal(parsed.Value.Database, parsed2.Value.Database);
Assert.Equal("cli", parsed2.Value.ApplicationName);
Assert.Equal("x y", parsed2.Value.Properties["param"]);
}
}

View file

@ -21,18 +21,22 @@ public sealed class ConnectionStringService : IConnectionStringService
} }
/// <summary> /// <summary>
/// Creates a service pre-configured with built-in codecs (Libpq, Npgsql, Url). /// Creates a service pre-configured with built-in codecs (Libpq, Npgsql, Url, Jdbc).
/// </summary> /// </summary>
public static ConnectionStringService CreateDefault() public static ConnectionStringService CreateDefault()
=> new(new IConnectionStringCodec[] { new LibpqCodec(), new NpgsqlCodec(), new UrlCodec() }); => new(new IConnectionStringCodec[] { new LibpqCodec(), new NpgsqlCodec(), new UrlCodec(), new JdbcCodec() });
public Result<ConnStringFormat> DetectFormat(string input) public Result<ConnStringFormat> DetectFormat(string input)
{ {
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
return Result.Fail<ConnStringFormat>("Empty input"); return Result.Fail<ConnStringFormat>("Empty input");
// URL: postgresql:// or postgres:// // URL: postgresql:// or postgres:// or JDBC jdbc:postgresql://
var trimmed = input.TrimStart(); var trimmed = input.TrimStart();
if (trimmed.StartsWith("jdbc:postgresql://", StringComparison.OrdinalIgnoreCase))
{
return Result.Ok(ConnStringFormat.Jdbc);
}
if (trimmed.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase) || if (trimmed.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase)) trimmed.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase))
{ {

View file

@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using FluentResults;
using Npgsql;
namespace pgLabII.PgUtils.ConnectionStrings;
/// <summary>
/// Codec for JDBC PostgreSQL URLs: jdbc:postgresql://host1[:port1][,hostN[:portN]]/[database]?param=value&...
/// - Supports multiple hosts with optional per-host ports, IPv6 bracketed literals.
/// - Percent-decodes database and query param values on parse; encodes on format.
/// - Recognizes sslmode/ssl, applicationName, loginTimeout/connectTimeout and maps to descriptor.
/// - Preserves unrecognized parameters in Properties.
/// </summary>
public sealed class JdbcCodec : IConnectionStringCodec
{
public ConnStringFormat Format => ConnStringFormat.Jdbc;
public string FormatName => "JDBC";
public Result<ConnectionDescriptor> TryParse(string input)
{
try
{
if (string.IsNullOrWhiteSpace(input))
return Result.Fail<ConnectionDescriptor>("Empty JDBC url");
var trimmed = input.Trim();
if (!trimmed.StartsWith("jdbc:postgresql://", StringComparison.OrdinalIgnoreCase))
return Result.Fail<ConnectionDescriptor>("JDBC url must start with jdbc:postgresql://");
// Strip scheme
var rest = trimmed.Substring("jdbc:postgresql://".Length);
// Split authority and path+query
string authority = rest;
string pathAndQuery = string.Empty;
var slashIdx = rest.IndexOf('/');
if (slashIdx >= 0)
{
authority = rest.Substring(0, slashIdx);
pathAndQuery = rest.Substring(slashIdx); // includes '/'
}
var builder = new ConnectionDescriptorBuilder();
// Parse hosts (comma separated)
foreach (var part in SplitHosts(authority))
{
if (string.IsNullOrWhiteSpace(part)) continue;
ParseHostPort(part, out var host, out ushort? port);
if (!string.IsNullOrEmpty(host)) builder.AddHost(host!, port);
}
// Parse database and query
string? database = null;
string query = string.Empty;
if (!string.IsNullOrEmpty(pathAndQuery))
{
int qIdx = pathAndQuery.IndexOf('?');
var path = qIdx >= 0 ? pathAndQuery.Substring(0, qIdx) : pathAndQuery;
query = qIdx >= 0 ? pathAndQuery.Substring(qIdx + 1) : string.Empty;
if (path.Length > 0)
{
if (path[0] == '/') path = path.Substring(1);
if (path.Length > 0)
database = Uri.UnescapeDataString(path);
}
}
if (!string.IsNullOrEmpty(database)) builder.Database = database;
var queryDict = ParseQuery(query);
// Map known properties
if (TryFirst(queryDict, out var ssl, "sslmode", "ssl"))
builder.SslMode = ParseSslMode(ssl);
if (TryFirst(queryDict, out var app, "applicationName", "application_name"))
builder.ApplicationName = app;
if (TryFirst(queryDict, out var tout, "loginTimeout", "connectTimeout", "connect_timeout"))
{
if (int.TryParse(tout, NumberStyles.Integer, CultureInfo.InvariantCulture, out var t))
builder.TimeoutSeconds = t;
}
// Preserve extras
var mapped = new HashSet<string>(new[] { "sslmode", "ssl", "applicationName", "application_name", "loginTimeout", "connectTimeout", "connect_timeout" }, StringComparer.OrdinalIgnoreCase);
foreach (var kv in queryDict)
{
if (!mapped.Contains(kv.Key))
builder.Properties[kv.Key] = kv.Value;
}
return Result.Ok(builder.Build());
}
catch (Exception ex)
{
return Result.Fail<ConnectionDescriptor>(ex.Message);
}
}
public Result<string> TryFormat(ConnectionDescriptor descriptor)
{
try
{
var sb = new StringBuilder();
sb.Append("jdbc:postgresql://");
if (descriptor.Hosts != null && descriptor.Hosts.Count > 0)
{
sb.Append(string.Join(',', descriptor.Hosts.Select(FormatHost)));
}
// Path with database
if (!string.IsNullOrEmpty(descriptor.Database))
{
sb.Append('/');
sb.Append(Uri.EscapeDataString(descriptor.Database));
}
// Query parameters
var qp = new List<(string k, string v)>();
if (descriptor.SslMode.HasValue)
{
qp.Add(("sslmode", FormatSslMode(descriptor.SslMode.Value)));
}
if (!string.IsNullOrEmpty(descriptor.ApplicationName))
{
qp.Add(("applicationName", descriptor.ApplicationName));
}
if (descriptor.TimeoutSeconds.HasValue)
{
qp.Add(("connectTimeout", descriptor.TimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture)));
}
// Add extras not already present
var emitted = new HashSet<string>(qp.Select(x => x.k), StringComparer.OrdinalIgnoreCase);
foreach (var kv in descriptor.Properties)
{
if (!emitted.Contains(kv.Key))
qp.Add((kv.Key, kv.Value));
}
if (qp.Count > 0)
{
sb.Append('?');
sb.Append(string.Join('&', qp.Select(p => Uri.EscapeDataString(p.k) + "=" + Uri.EscapeDataString(p.v ?? string.Empty))));
}
return Result.Ok(sb.ToString());
}
catch (Exception ex)
{
return Result.Fail<string>(ex.Message);
}
}
private static IEnumerable<string> SplitHosts(string authority)
{
return authority.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
private static string FormatHost(HostEndpoint h)
{
var host = h.Host;
if (host.Contains(':') && !host.StartsWith("["))
{
// IPv6 literal must be bracketed
host = "[" + host + "]";
}
return h.Port.HasValue ? host + ":" + h.Port.Value.ToString(CultureInfo.InvariantCulture) : host;
}
private static void ParseHostPort(string hostPart, out string host, out ushort? port)
{
host = hostPart;
port = null;
if (string.IsNullOrWhiteSpace(hostPart)) return;
if (hostPart[0] == '[')
{
int end = hostPart.IndexOf(']');
if (end > 0)
{
host = hostPart.Substring(1, end - 1);
if (end + 1 < hostPart.Length && hostPart[end + 1] == ':')
{
var ps = hostPart.Substring(end + 2);
if (ushort.TryParse(ps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
port = p;
}
}
return;
}
int colon = hostPart.LastIndexOf(':');
if (colon > 0 && colon < hostPart.Length - 1)
{
var ps = hostPart.Substring(colon + 1);
if (ushort.TryParse(ps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
{
host = hostPart.Substring(0, colon);
port = p;
}
}
}
private static Dictionary<string, string> ParseQuery(string query)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(query)) return dict;
foreach (var kv in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var parts = kv.Split('=', 2);
var key = Uri.UnescapeDataString(parts[0]);
var val = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty;
dict[key] = val;
}
return dict;
}
private static bool TryFirst(Dictionary<string, string> dict, out string value, params string[] keys)
{
foreach (var k in keys)
{
if (dict.TryGetValue(k, out value)) return true;
}
value = string.Empty;
return false;
}
private static SslMode ParseSslMode(string s)
{
switch (s.Trim().ToLowerInvariant())
{
case "disable": return SslMode.Disable;
case "allow": return SslMode.Allow;
case "prefer": return SslMode.Prefer;
case "require": return SslMode.Require;
case "verify-ca":
case "verifyca": return SslMode.VerifyCA;
case "verify-full":
case "verifyfull": return SslMode.VerifyFull;
default: throw new ArgumentException($"Not a valid SSL Mode: {s}");
}
}
private static string FormatSslMode(SslMode mode)
{
return mode switch
{
SslMode.Disable => "disable",
SslMode.Allow => "allow",
SslMode.Prefer => "prefer",
SslMode.Require => "require",
SslMode.VerifyCA => "verify-ca",
SslMode.VerifyFull => "verify-full",
_ => "prefer"
};
}
private sealed class ConnectionDescriptorBuilder
{
public List<HostEndpoint> Hosts { get; } = new();
public string? Database { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public SslMode? SslMode { get; set; }
public string? ApplicationName { get; set; }
public int? TimeoutSeconds { get; set; }
public Dictionary<string, string> Properties { get; } = new(StringComparer.OrdinalIgnoreCase);
public void AddHost(string host, ushort? port)
{
if (string.IsNullOrWhiteSpace(host)) return;
Hosts.Add(new HostEndpoint { Host = host.Trim(), Port = port });
}
public ConnectionDescriptor Build()
{
return new ConnectionDescriptor
{
Hosts = Hosts,
Database = Database,
Username = Username,
Password = Password,
SslMode = SslMode,
ApplicationName = ApplicationName,
TimeoutSeconds = TimeoutSeconds,
Properties = Properties
};
}
}
}

View file

@ -2,20 +2,23 @@
This document tracks the plan for supporting multiple PostgreSQL connection string formats, converting between them, and mapping to/from a canonical model. This document tracks the plan for supporting multiple PostgreSQL connection string formats, converting between them, and mapping to/from a canonical model.
## Current Status (2025-08-30) ## Current Status (2025-08-31)
Implemented: Implemented:
- Abstractions: `ConnStringFormat`, `HostEndpoint`, `ConnectionDescriptor`, `IConnectionStringCodec`, `IConnectionStringService`. - Abstractions: `ConnStringFormat`, `HostEndpoint`, `ConnectionDescriptor`, `IConnectionStringCodec`, `IConnectionStringService`.
- Codecs: - Codecs:
- `LibpqCodec` (libpq): parse/format; multi-host; `sslmode`, `application_name`, `connect_timeout`; quoting/escaping; preserves extras. - `LibpqCodec` (libpq): parse/format; multi-host; `sslmode`, `application_name`, `connect_timeout`; quoting/escaping; preserves extras.
- `NpgsqlCodec` (.NET/Npgsql): parse/format; alias recognition; multi-host with single or per-host ports; `SSL Mode`, `Application Name`, `Timeout`; double-quote rules; preserves extras. - `NpgsqlCodec` (.NET/Npgsql): parse/format; alias recognition; multi-host with single or per-host ports; `SSL Mode`, `Application Name`, `Timeout`; double-quote rules; preserves extras.
- Tests for both codecs: parse, format, round-trip, edge quoting. - `UrlCodec` (postgresql://): parse/format; userinfo, multi-host with per-host ports, IPv6 `[::1]` handling, database path, percent-decoding/encoding, common params mapping, preserves extras.
- Composite `ConnectionStringService` (detect + convert) composing Libpq, Npgsql, and Url codecs.
- Mapping helpers to/from `ServerConfiguration` (primary host/port, database, SSL mode) with sensible defaults.
- Tests:
- Unit tests for Libpq, Npgsql, and Url codecs (parse/format/round-trip/edge quoting and percent-encoding).
- ConnectionStringService detection/conversion tests.
- ServerConfiguration mapping tests.
Not yet implemented: Not yet implemented:
- URL (postgresql://) codec ✓
- JDBC (jdbc:postgresql://) codec - JDBC (jdbc:postgresql://) codec
- Composite `ConnectionStringService` (detect + convert) ✓
- Mapping helpers to/from `ServerConfiguration`
## Updated Plan ## Updated Plan
@ -26,26 +29,26 @@ Not yet implemented:
- Npgsql codec (parse/format; aliases, multi-host/ports, quoting, ssl mode, timeout, extras). ✓ - Npgsql codec (parse/format; aliases, multi-host/ports, quoting, ssl mode, timeout, extras). ✓
- URL (postgresql://) codec (parse/format; userinfo, host[:port], db, query params, percent-encoding). ✓ - URL (postgresql://) codec (parse/format; userinfo, host[:port], db, query params, percent-encoding). ✓
- JDBC (jdbc:postgresql://) codec (parse/format; hosts, ports, db, properties; URL-like semantics). - JDBC (jdbc:postgresql://) codec (parse/format; hosts, ports, db, properties; URL-like semantics).
4. Composite conversion service: 4. Composite conversion service. ✓
- Implement `ConnectionStringService` composing codecs, detecting formats, converting via `ConnectionDescriptor`, and resolving alias priorities. 5. Mapping with application model. ✓
5. Mapping with application model:
- Add mapping utilities between `ConnectionDescriptor` and `ServerConfiguration` (primary host/port, db, SSL mode), with sensible defaults.
6. Validation and UX: 6. Validation and UX:
- Validation for malformed inputs & edge cases (mismatched host/port counts, invalid SSL mode, missing db/host, IPv6 bracket handling). - Validation for malformed inputs & edge cases (mismatched host/port counts, invalid SSL mode, missing db/host, IPv6 bracket handling). ✓
- Ensure sensitive fields (password) are masked in logs/preview. - Ensure sensitive fields (password) are masked in logs/preview). ✓
7. Tests: 7. Tests:
- Unit tests for URL and JDBC codecs; composite service detect/convert; mapping functions; cross-format round-trips; edge cases (spaces, quotes, unicode, IPv6, percent-encoding). - Unit tests for URL codec (parse/format/round-trip/percent-encoding). ✓
- Tests for composite service detect/convert; mapping functions; cross-format round-trips; edge cases (spaces, quotes, unicode, IPv6, percent-encoding). ✓
- Unit tests for JDBC codec.
8. Documentation: 8. Documentation:
- Keep this plan updated and enrich XML docs on codecs/service including alias mappings and quoting/escaping rules per format. - Keep this plan updated and enrich XML docs on codecs/service including alias mappings and quoting/escaping rules per format. *
## Next Small Step ## Next Small Step
Implement the URL (postgresql://) codec with unit tests. Scope: Implement the JDBC (jdbc:postgresql://) codec with unit tests. Scope:
- Parse: `postgresql://[user[:password]@]host1[:port1][,hostN[:portN]]/[database]?param=value&...` - Parse: `jdbc:postgresql://host1[:port1][,hostN[:portN]]/[database]?param=value&...`
- Support percent-decoding for user, password, database, and query values. - Support multiple hosts with optional per-host ports; IPv6 bracket handling.
- Handle IPv6 literals in `[::1]` form; allow multiple hosts with optional per-host ports. - Recognize common properties (sslmode/SSL, applicationName, loginTimeout/connectTimeout) and preserve unrecognized properties.
- Map common params: `sslmode`, `application_name`, `connect_timeout` and preserve other query params in `Properties`. - Ensure URL-like semantics consistent with UrlCodec percent-decoding/encoding.
- Format: Build a URL using percent-encoding where required; emit multi-hosts and parameters from `Properties` not already emitted. - Format: Build JDBC URL from ConnectionDescriptor; emit multi-hosts and properties from `Properties` not already emitted.
- Tests: basic parse/format, quoting/percent-encoding, multi-host with mixed ports, round-trips. - Tests: basic parse/format, multi-host with mixed ports, percent-encoding, round-trips; cross-format conversions via ConnectionStringService.
After that, implement the composite `ConnectionStringService` to detect/convert across libpq, Npgsql, and URL formats. After that, consider minor documentation polish and any gaps in edge-case validation discovered while adding JDBC support.

View file

@ -35,7 +35,8 @@ public class EditServerConfigurationViewModel : ViewModelBase
Auto, Auto,
Libpq, Libpq,
Npgsql, Npgsql,
Url Url,
Jdbc
} }
private ForcedFormatOption _forcedFormat = ForcedFormatOption.Auto; private ForcedFormatOption _forcedFormat = ForcedFormatOption.Auto;
@ -126,6 +127,7 @@ public class EditServerConfigurationViewModel : ViewModelBase
ForcedFormatOption.Libpq => new LibpqCodec(), ForcedFormatOption.Libpq => new LibpqCodec(),
ForcedFormatOption.Npgsql => new NpgsqlCodec(), ForcedFormatOption.Npgsql => new NpgsqlCodec(),
ForcedFormatOption.Url => new UrlCodec(), ForcedFormatOption.Url => new UrlCodec(),
ForcedFormatOption.Jdbc => new JdbcCodec(),
_ => new UrlCodec() _ => new UrlCodec()
}; };
var r = codec.TryParse(InputConnectionString); var r = codec.TryParse(InputConnectionString);
@ -153,6 +155,7 @@ public class EditServerConfigurationViewModel : ViewModelBase
ForcedFormatOption.Libpq => ConnStringFormat.Libpq, ForcedFormatOption.Libpq => ConnStringFormat.Libpq,
ForcedFormatOption.Npgsql => ConnStringFormat.Npgsql, ForcedFormatOption.Npgsql => ConnStringFormat.Npgsql,
ForcedFormatOption.Url => ConnStringFormat.Url, ForcedFormatOption.Url => ConnStringFormat.Url,
ForcedFormatOption.Jdbc => ConnStringFormat.Jdbc,
_ => ConnStringFormat.Url _ => ConnStringFormat.Url
}; };
} }

View file

@ -51,16 +51,17 @@
<!-- Input Connection String --> <!-- Input Connection String -->
<StackPanel Grid.Row="1" Margin="0,12,0,0" Spacing="6"> <StackPanel Grid.Row="1" Margin="0,12,0,0" Spacing="6">
<TextBlock FontWeight="Bold" Text="Connection string input"/> <TextBlock FontWeight="Bold" Text="Connection string input"/>
<TextBlock Text="Paste an existing connection string (libpq, Npgsql, or URL)."/> <TextBlock Text="Paste an existing connection string (libpq, Npgsql, URL, or JDBC)."/>
<TextBox TextWrapping="Wrap" AcceptsReturn="True" MinHeight="60" Text="{Binding InputConnectionString}"/> <TextBox TextWrapping="Wrap" AcceptsReturn="True" MinHeight="60" Text="{Binding InputConnectionString}"/>
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" VerticalAlignment="Center"> <Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Interpret as"/> <TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Interpret as"/>
<!-- Bind SelectedIndex to enum (Auto=0, Libpq=1, Npgsql=2, Url=3) --> <!-- Bind SelectedIndex to enum (Auto=0, Libpq=1, Npgsql=2, Url=3, Jdbc=4) -->
<ComboBox Grid.Column="1" SelectedIndex="{Binding ForcedFormat}"> <ComboBox Grid.Column="1" SelectedIndex="{Binding ForcedFormat}">
<ComboBoxItem>Auto</ComboBoxItem> <ComboBoxItem>Auto</ComboBoxItem>
<ComboBoxItem>Libpq</ComboBoxItem> <ComboBoxItem>Libpq</ComboBoxItem>
<ComboBoxItem>Npgsql</ComboBoxItem> <ComboBoxItem>Npgsql</ComboBoxItem>
<ComboBoxItem>URL</ComboBoxItem> <ComboBoxItem>URL</ComboBoxItem>
<ComboBoxItem>JDBC</ComboBoxItem>
</ComboBox> </ComboBox>
<TextBlock Grid.Column="2" Margin="12,0,8,0" VerticalAlignment="Center" Text="Detected:"/> <TextBlock Grid.Column="2" Margin="12,0,8,0" VerticalAlignment="Center" Text="Detected:"/>
<TextBlock Grid.Column="3" VerticalAlignment="Center" Text="{Binding DetectedFormat}"/> <TextBlock Grid.Column="3" VerticalAlignment="Center" Text="{Binding DetectedFormat}"/>
@ -73,11 +74,12 @@
<TextBlock FontWeight="Bold" Text="Connection string output"/> <TextBlock FontWeight="Bold" Text="Connection string output"/>
<Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center"> <Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Format"/> <TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Format"/>
<!-- ConnStringFormat: Libpq=0, Npgsql=1, Url=2 --> <!-- ConnStringFormat: Libpq=0, Npgsql=1, Url=2, Jdbc=3 -->
<ComboBox Grid.Column="1" SelectedIndex="{Binding OutputFormat}"> <ComboBox Grid.Column="1" SelectedIndex="{Binding OutputFormat}">
<ComboBoxItem>Libpq</ComboBoxItem> <ComboBoxItem>Libpq</ComboBoxItem>
<ComboBoxItem>Npgsql</ComboBoxItem> <ComboBoxItem>Npgsql</ComboBoxItem>
<ComboBoxItem>URL</ComboBoxItem> <ComboBoxItem>URL</ComboBoxItem>
<ComboBoxItem>JDBC</ComboBoxItem>
</ComboBox> </ComboBox>
<Button Grid.Column="2" Margin="12,0,0,0" Content="Generate" Command="{Binding GenerateConnectionStringCommand}"/> <Button Grid.Column="2" Margin="12,0,0,0" Content="Generate" Command="{Binding GenerateConnectionStringCommand}"/>
<Button Grid.Column="3" Margin="6,0,0,0" Content="Copy" Command="{Binding CopyOutputConnectionStringCommand}"/> <Button Grid.Column="3" Margin="6,0,0,0" Content="Copy" Command="{Binding CopyOutputConnectionStringCommand}"/>