Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<PackageVersion Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19" />
<PackageVersion Include="DistributedLock.Postgres" Version="1.3.0" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.39" />
<PackageVersion Include="JasperFx" Version="2.0.0-alpha.1" />
<PackageVersion Include="JasperFx.Events" Version="2.0.0-alpha.1" />
<PackageVersion Include="JasperFx" Version="2.0.0-alpha.8" />
<PackageVersion Include="JasperFx.Events" Version="2.0.0-alpha.3" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.1" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<!-- EF Core versions are framework-conditional - see below -->
Expand Down
3 changes: 2 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export default withMermaid(
{ text: 'Schema Migrations', link: '/core/schema-migrations' },
{ text: 'Command Builders & Batching', link: '/core/command-builders' },
{ text: 'Extension Methods', link: '/core/extension-methods' },
{ text: 'Multi-Tenancy', link: '/core/multi-tenancy' }
{ text: 'Multi-Tenancy', link: '/core/multi-tenancy' },
{ text: 'Provider Trait Matrix', link: '/core/provider-trait-matrix' }
]
},
{
Expand Down
139 changes: 139 additions & 0 deletions docs/core/provider-trait-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Provider Trait Matrix

Weasel supports five database providers — PostgreSQL, SQL Server, Oracle,
MySQL, and SQLite — each with subtly different feature sets. The audit at
[#270](https://github.com/JasperFx/weasel/issues/270) documented this matrix
explicitly so users can write provider-agnostic schema code with confidence
about what works everywhere versus what's provider-specific.

## At a glance

| Feature | PostgreSQL | SQL Server | Oracle | MySQL | SQLite |
| --- | --- | --- | --- | --- | --- |
| Default schema | `public` | `dbo` | `WEASEL` | `public` | `main` |
| Identifier quoting | `"name"` | `[name]` | `"NAME"` | `` `name` `` | `"name"` |
| Identifier case | Sensitive | Folded | Folded (upper) | Folded (lower) | Folded |
| Max identifier length | 63 | 128 | 128 | 64 | (unlimited) |
| Auto-increment SQL | `SERIAL` | `IDENTITY(1,1)` | `GENERATED…AS IDENTITY` | `AUTO_INCREMENT` | `AUTOINCREMENT` |
| `ALTER TABLE ADD COLUMN` | ✓ | ✓ | ✓ | ✓ | ✓ |
| `ALTER TABLE DROP COLUMN` | ✓ | ✓ | ✓ | ✓ | ✓ (3.35+) |
| `ALTER TABLE ALTER COLUMN` | ✓ | ✓ | ✓ | ✓ | ✗ (recreate) |
| `ALTER TABLE ADD CONSTRAINT FK` | ✓ | ✓ | ✓ | ✓ | ✗ (inline only) |
| `ALTER TABLE DROP CONSTRAINT FK` | ✓ | ✓ | ✓ | ✓ | ✗ (recreate) |
| Cascading actions: `RESTRICT` | ✓ | ✗ (→ NoAction) | ✗ (→ NoAction) | ✓ | ✓ |
| Native sequences | ✓ | ✓ | ✓ | Table-emulated | Table-emulated |
| Views | ✓ + materialized | ✓ | ✓ | ✓ | ✓ (read-only) |
| Functions / Stored Procedures | ✓ (PL/pgSQL) | ✓ (T-SQL) | ✓ (PL/SQL) | (limited) | Connection-scoped |
| Partitioning | ✓ (hash/range/list) | ✓ (range) | ✓ | ✓ | ✗ |
| JSON storage | `jsonb` native | `nvarchar(max)` | `CLOB` | `JSON` | `TEXT` (JSON1) |
| Array columns | ✓ | ✗ | ✗ | ✗ | ✗ (except `byte[]`) |
| Statement terminator | `;` | `;` | `/` (PL/SQL blocks) | `;` | `;` |

## Identity / auto-increment

In 9.0 every provider exposes a canonical `.AutoIncrement()` fluent method on
`Table.ColumnExpression`. Previously each provider used a different spelling;
the old names are kept as `[Obsolete]` aliases for one major-version cycle.

| Provider | Canonical 9.0 | Pre-9.0 alias (`[Obsolete]`) | SQL Emitted |
| --- | --- | --- | --- |
| PostgreSQL | `AutoIncrement()` | `Serial()` | `SERIAL` |
| PostgreSQL | `BigAutoIncrement()` | `BigSerial()` | `BIGSERIAL` |
| PostgreSQL | `SmallAutoIncrement()` | `SmallSerial()` | `SMALLSERIAL` |
| SQL Server | `AutoIncrement()` | `AutoNumber()` | `IDENTITY(1,1)` |
| Oracle | `AutoIncrement()` | `AutoNumber()` | `GENERATED BY DEFAULT AS IDENTITY` |
| MySQL | `AutoIncrement()` | `AutoNumber()` | `AUTO_INCREMENT` |
| SQLite | `AutoIncrement()` | (no change) | `AUTOINCREMENT` |

```csharp
// Same code compiles against any of the five providers
table.AddColumn<int>("id").AsPrimaryKey().AutoIncrement();
```

## Identifier comparison

`Table.ColumnFor` / `HasColumn` / `IndexFor` / `HasIndex` use a per-provider
`NameComparison`:

| Provider | `NameComparison` | Rationale |
| --- | --- | --- |
| PostgreSQL | `Ordinal` | PG is case-sensitive when quoted (the default for Weasel-generated DDL) |
| SQL Server | `OrdinalIgnoreCase` | SS identifier comparison is case-insensitive by default |
| Oracle | `OrdinalIgnoreCase` | Oracle folds unquoted identifiers to upper case |
| MySQL | `OrdinalIgnoreCase` | MySQL identifier comparison is case-insensitive on most platforms |
| SQLite | `OrdinalIgnoreCase` | SQLite identifiers are case-folded |

`RemoveColumn` is always case-insensitive on every provider — every concrete
provider used `EqualsIgnoreCase` historically.

## Primary-key name defaults

When you don't supply a `PrimaryKeyName`, each provider generates one with
its own convention:

| Provider | Default `PrimaryKeyName` |
| --- | --- |
| PostgreSQL | `pkey_{table}_{cols}` |
| SQL Server | `pkey_{table}_{cols}` |
| Oracle | `pk_{table}_{cols}` |
| MySQL | `pk_{table}_{cols}` |
| SQLite | `pk_{table}` |

## Schema-object base types

Most schema objects share a base class in `Weasel.Core`:

| Object | Core base | Notes |
| --- | --- | --- |
| Tables | `TableBase<TColumn, TIndex, TForeignKey>` | New in 9.0 (#270 step 9) |
| Foreign keys | `ForeignKeyBase` | Owns `Parse`, `LinkColumns`, `Equals`, `ReadReferentialActions` |
| Sequences | `SequenceBase` | PG / SS / Oracle native, MySQL / SQLite table-emulated |
| Functions | `FunctionBase` | PG + SS only; Oracle uses procedures; SQLite functions are connection-scoped |
| Views | `ViewBase` | All providers; SQLite is read-only |
| Indexes | `INamed` only | Provider-specific (PG is ~4× richer than the others) |
| Table columns | `ITableColumn` only | Provider-specific declaration formatting |

## DDL syntax strategy

The shared CREATE / DROP algorithm is gradually migrating to consume a
[`IDdlSyntaxStrategy`](../../../src/Weasel.Core/IDdlSyntaxStrategy.cs) object
per provider (#270 step 8). Currently routed through it:

- `WriteDropTable` (PG appends `CASCADE`, SQLite doesn't)
- `WriteCreateTableHeader` (with / without `IF NOT EXISTS`)

The remainder of `WriteCreateStatement` is still per-provider while the
strategy shape settles. Plan is to migrate column / PK / FK emission as a
follow-up.

## Constructor pattern

All five `Table` classes share the same constructor signatures:

```csharp
new Table(DbObjectName identifier); // identifier wrapped in provider-specific subclass internally
new Table(string tableName); // parsed via provider's DbObjectName.Parse
```

The PostgreSQL and SQLite implementations wrap the supplied `DbObjectName` in
`PostgresqlObjectName` / `SqliteObjectName` respectively; SS / Oracle / MySQL
pass through the `DbObjectName` directly. Either way, the public surface is
uniform — polymorphic schema-building code doesn't need to know which provider
it's targeting until DDL is emitted.

## When you do need provider-specific code

Provider-specific extensions stay on the concrete `Table` / `ColumnExpression`:

- **PostgreSQL**: `MaterializedView`, `Partitioning` (hash / range / list),
`IgnorePartitionsInMigration`, `FullTextIndex`, `UpsertFunction`,
`NpgsqlRange<T>` columns
- **SQL Server**: `SqlServerPartitioning` (range), `PartitionByRange`
- **Oracle**: `PartitionStrategy`, `PartitionExpressions`
- **MySQL**: `Engine`, `Charset`, `Collation`, `PartitionCount`,
`AddFulltextIndex`
- **SQLite**: `WithoutRowId`, `StrictTypes`, `GeneratedAs`, JSON expression
indexes via `ForJsonPath`

Polymorphic code uses the shared `ITable` / `TableBase` surface; code that
needs provider extras casts to the concrete `Table` type.
2 changes: 1 addition & 1 deletion src/DocSamples/MySqlSamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void mysql_define_table()
#region sample_mysql_define_table
var table = new Table("users");

table.AddColumn<int>("id").AsPrimaryKey().AutoNumber();
table.AddColumn<int>("id").AsPrimaryKey().AutoIncrement();
table.AddColumn<string>("name").NotNull();
table.AddColumn<string>("email").NotNull().AddIndex(idx => idx.IsUnique = true);
table.AddColumn<DateTime>("created_at");
Expand Down
4 changes: 2 additions & 2 deletions src/DocSamples/SqlServerSamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void ss_define_table()
#region sample_ss_define_table
var table = new Table("dbo.users");

table.AddColumn<int>("id").AsPrimaryKey().AutoNumber();
table.AddColumn<int>("id").AsPrimaryKey().AutoIncrement();
table.AddColumn<string>("name").NotNull();
table.AddColumn<string>("email").NotNull().AddIndex(idx => idx.IsUnique = true);
table.AddColumn<DateTime>("created_at").DefaultValueByExpression("GETUTCDATE()");
Expand All @@ -64,7 +64,7 @@ public void ss_foreign_keys()
{
#region sample_ss_foreign_keys
var orders = new Table("dbo.orders");
orders.AddColumn<int>("id").AsPrimaryKey().AutoNumber();
orders.AddColumn<int>("id").AsPrimaryKey().AutoIncrement();
orders.AddColumn<int>("user_id").NotNull()
.ForeignKeyTo("dbo.users", "id", onDelete: Weasel.SqlServer.CascadeAction.Cascade);
orders.AddColumn<decimal>("total").NotNull();
Expand Down
48 changes: 48 additions & 0 deletions src/Weasel.Core/DatabaseProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Data.Common;
using ImTools;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Weasel.Core.Names;

namespace Weasel.Core;
Expand Down Expand Up @@ -170,6 +171,53 @@ public void RegisterMapping(Type type, string databaseType, TParameterType? para
ParameterTypeMemo.Swap(d => d.AddOrUpdate(type, parameterType));
}

/// <summary>
/// Shared memo lookup for the database-type string of a CLR type, with the
/// standard nullable-promote fallback (if <c>T?</c> is asked but <c>T</c> is
/// in the memo, copy the entry to <c>T?</c> and return it). Returns null
/// when neither the type nor its inner-nullable is mapped — subclasses then
/// apply provider-specific fallbacks (PG queries Npgsql's plugin type map,
/// SS/Oracle/MySQL throw, SQLite returns null to signal "TEXT/JSON").
/// </summary>
protected string? ResolveDatabaseTypeFromMemo(Type type)
{
if (DatabaseTypeMemo.Value.TryFind(type, out var value))
{
return value;
}

if (type.IsNullable() && DatabaseTypeMemo.Value.TryFind(type.GetInnerTypeFromNullable(), out var inner))
{
DatabaseTypeMemo.Swap(d => d.AddOrUpdate(type, inner));
return inner;
}

return null;
}

/// <summary>
/// Shared memo lookup for the provider parameter-type enum of a CLR type,
/// mirror of <see cref="ResolveDatabaseTypeFromMemo" />. Subclasses apply
/// their own fallback when this returns null (PG queries Npgsql plugin;
/// SS returns <c>Variant</c>; Oracle returns <c>Varchar2</c>; MySQL returns
/// <c>VarChar</c>; SQLite returns <c>Text</c>).
/// </summary>
protected TParameterType? ResolveParameterTypeFromMemo(Type type)
{
if (ParameterTypeMemo.Value.TryFind(type, out var value))
{
return value;
}

if (type.IsNullable() && ParameterTypeMemo.Value.TryFind(type.GetInnerTypeFromNullable(), out var inner))
{
ParameterTypeMemo.Swap(d => d.AddOrUpdate(type, inner));
return inner;
}

return null;
}

protected abstract Type[] determineClrTypesForParameterType(TParameterType dbType);

public TParameter AddParameter(TCommand command, object? value, TParameterType? dbType = null)
Expand Down
Loading