Chapter 06 · Functions, NULLs, and Conditional Logic
Vendor Portability and Function Equivalents
Separate standard SQL concepts from dialect-specific spellings so queries can be maintained across database products.
Learning outcomes
SQL standards define many concepts, but production engines expose different function names, operators, argument orders, type rules, and date/time models. Portability begins by identifying which layer owns those differences.
Compare common string, date/time, NULL, and conversion idioms across major SQL engines.
Separate portable expressions from dialect-specific implementations.
Choose a strategy for multi-database applications and analytical pipelines.
Test return types and edge cases rather than assuming equivalent names mean equivalent behavior.
Portability has several dimensions
Name
The same operation may have a different function name.
Syntax
Argument order, units, and operators may differ.
Type
Return type, precision, collation, and NULL resolution can differ.
Behavior
Boundary rules, time zones, rounding, and invalid-input handling can differ.
Portability is a tested mapping from meaning to engine behavior, not a search-and-replace exercise.
String-function equivalents
| Intent | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
| Length in characters | LENGTH(x) | CHAR_LENGTH(x) or LENGTH(x) | CHAR_LENGTH(x) | LEN(x) | LENGTH(x) |
| Substring | SUBSTR(x,s,n) | SUBSTRING(x FROM s FOR n) | SUBSTRING(x,s,n) | SUBSTRING(x,s,n) | SUBSTR(x,s,n) |
| Concatenate | a || b | a || b or CONCAT | CONCAT(a,b) | CONCAT(a,b) or a + b | a || b or CONCAT |
| Trim | TRIM(x) | TRIM(x) | TRIM(x) | TRIM(x) | TRIM(x) |
| Case conversion | LOWER/UPPER | LOWER/UPPER | LOWER/UPPER | LOWER/UPPER | LOWER/UPPER |
SELECT SUBSTR(product_name, 1, 12) AS short_name, category || ': ' || product_name AS labelFROM product;SELECT SUBSTRING(product_name FROM 1 FOR 12) AS short_name, category || ': ' || product_name AS labelFROM product;SELECT SUBSTRING(product_name, 1, 12) AS short_name, CONCAT(category, ': ', product_name) AS labelFROM product;SELECT SUBSTRING(product_name, 1, 12) AS short_name, CONCAT(category, ': ', product_name) AS labelFROM product;Even apparently similar operations can differ for trailing spaces, byte length, Unicode grapheme handling, or NULL concatenation. Test the exact data contract.
Date/time equivalents
| Intent | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
| Current date | DATE('now') | CURRENT_DATE | CURRENT_DATE | CAST(GETDATE() AS date) | CURRENT_DATE |
| Extract year | STRFTIME('%Y',x) | EXTRACT(YEAR FROM x) | EXTRACT(YEAR FROM x) or YEAR(x) | DATEPART(year,x) | EXTRACT(YEAR FROM x) |
| Add days | DATE(x,'+7 days') | x + INTERVAL '7 days' | DATE_ADD(x, INTERVAL 7 DAY) | DATEADD(day,7,x) | x + 7 for DATE values |
| Format date | STRFTIME(format,x) | TO_CHAR(x,format) | DATE_FORMAT(x,format) | CONVERT/FORMAT with tradeoffs | TO_CHAR(x,format) |
Engines differ in timestamp types, time-zone semantics, interval types, daylight-saving transitions, end-of-month behavior, and invalid date handling.
NULL and conditional equivalents
| Intent | Portable choice | Vendor alternatives |
|---|---|---|
| First non-NULL value | COALESCE(a,b,...) | SQLite IFNULL, MySQL IFNULL, SQL Server ISNULL, Oracle NVL |
| NULL when equal | NULLIF(a,b) | Widely supported with vendor-specific type details |
| Conditional value | CASE | MySQL IF, SQLite IIF, Oracle DECODE for narrower cases |
| NULL test | IS NULL | Do not replace with ordinary equality |
SELECT COALESCE(NULLIF(TRIM(email), ''), '[no email]') AS email_label, CASE WHEN credit_limit IS NULL THEN 'review' ELSE 'known' END AS limit_stateFROM customer;Vendor shortcuts can be useful in a single-engine system. Standard forms generally reduce migration effort and make intent clearer to a wider audience.
Conversion equivalents
SELECT CAST(unit_price AS INTEGER) AS whole_priceFROM product;SELECT CONVERT(varchar(10), sold_at, 23) AS sold_dateFROM sale;SELECT unit_price::integer AS whole_priceFROM product;CAST is the most portable core syntax. Vendor forms may add formatting styles, tolerant conversions, or shorthand, but those features bind the query to that engine.
| Engine feature | Example | Portability concern |
|---|---|---|
| SQL Server tolerant conversion | TRY_CAST / TRY_CONVERT | Returns NULL instead of failing for many invalid conversions. |
| PostgreSQL shorthand | x::type | Concise but non-standard. |
| Oracle formatting conversion | TO_CHAR/TO_DATE | Format models are Oracle-specific. |
| MySQL coercion | Context-dependent conversion | Permissive modes can hide invalid data. |
| SQLite affinity conversion | Dynamic storage classes | Result may differ from rigidly typed engines. |
Function return types are part of portability
SELECT TYPEOF(ROUND(49.95, 1)) AS rounded_type, TYPEOF(COALESCE(NULL, 0)) AS integer_fallback_type, TYPEOF(COALESCE(NULL, 0.0)) AS real_fallback_type, TYPEOF(DATE('2026-08-05')) AS date_function_type;SQLite's DATE function returns text. PostgreSQL, SQL Server, MySQL, and Oracle have dedicated temporal types and different expression type-resolution rules. Equivalent visible output does not imply equivalent result metadata.
Choose a portability architecture
| Strategy | Advantages | Costs |
|---|---|---|
| One SQL dialect | Use the engine fully and keep queries simple | Migration requires a deliberate rewrite. |
| Portable SQL subset | Shared queries run across several engines | Lowest-common-denominator features can reduce clarity or performance. |
| Dialect adapter layer | Each engine receives idiomatic SQL | More implementations and test cases. |
| Query builder or ORM | Centralizes syntax generation | Generated SQL still needs inspection and database expertise. |
| Database views/API boundary | Expose one stable contract to applications | Engine-specific database objects must be maintained. |
Do not pay permanent complexity for a hypothetical migration. Decide whether supporting multiple engines is an actual requirement, then test every supported dialect in continuous integration.
A practical compatibility matrix
operation: normalized email labelsemantic rule: trim; blank becomes missing; fallback label is [no email]result type: textnull input: [no email]non-ASCII test: requiredsqlite expression: COALESCE(NULLIF(TRIM(email), ''), '[no email]')postgresql expression: COALESCE(NULLIF(BTRIM(email), ''), '[no email]')mysql expression: COALESCE(NULLIF(TRIM(email), ''), '[no email]')sql_server expression: COALESCE(NULLIF(TRIM(email), ''), '[no email]')oracle expression: COALESCE(NULLIF(TRIM(email), ''), '[no email]')A matrix captures meaning, input cases, result metadata, and implementation—not merely function-name pairs.
Practice rewrite lab
- Write a product label in SQLite, PostgreSQL, MySQL, and SQL Server.
- Extract the sale year in each target dialect.
- Add seven days to a sale timestamp in each dialect.
- Use portable
COALESCE,NULLIF,CASE, andCASTwhere possible. - List tests for NULL, empty text, Unicode, leap dates, and invalid conversion input.
SELECT CAST(product_id AS VARCHAR(20)) AS product_id_text, COALESCE(NULLIF(TRIM(product_name), ''), '[unnamed]') AS product_label, CASE WHEN stock_qty = 0 THEN 'unavailable' ELSE 'available' END AS stock_stateFROM product;Common failures
Creating a function-name cheat sheet only
Names do not capture types, collations, NULL rules, and boundary behavior.
Assuming visible strings prove equivalence
Result metadata and time-zone meaning can still differ.
Mixing dialects in one query
A query containing PostgreSQL casts, MySQL date functions, and SQL Server TOP is not portable.
Choosing the lowest common denominator prematurely
This can make every implementation worse without a real multi-engine requirement.
Skipping cross-engine tests
Documentation comparison cannot replace execution against supported versions.
Summary and references
- Portability includes names, syntax, types, and behavior.
- Prefer standard
CASE,COALESCE,NULLIF, andCASTwhen they express the requirement well. - Date/time and string behavior require special cross-engine testing.
- Choose an architecture that matches the actual multi-database requirement.
- Maintain a semantic compatibility matrix and automated tests.