Chapter 04 · Schemas, Data Types, Keys, Constraints, and SQL Modes
SQL_MODE, Strictness, Implicit Conversion, Oracle Mode Awareness, and Data Quality
Make SQL_MODE a visible data-quality contract by testing strictness, coercion, date policy, grouping semantics and Oracle compatibility in isolated MariaDB sessions.
Learning outcomes
ServiceHub passes schema review, yet the same INSERT behaves
differently in two environments: development rejects an
oversized/invalid value, while an old import script coerces it
and continues with warnings. A reporting query is accepted on
one session and rejected on another. An Oracle-migration proof
of concept turns on sql_mode=ORACLE and
accidentally changes parsing expectations for unrelated code.
The schema did not change; the
SQL mode contract did.
SQL_MODE is a comma-separated set of behavioral
flags that can change parsing, validation, conversions and
compatibility semantics. It has GLOBAL and SESSION scope:
changing GLOBAL affects new sessions, not magically every
already-connected client. MariaDB’s current default includes
strict behavior such as STRICT_TRANS_TABLES, but
applications should inspect and test the exact mode instead of
assuming defaults from another MariaDB/MySQL version.
Inspect GLOBAL and SESSION SQL_MODE and explain inheritance to new connections.
Demonstrate strict versus permissive conversion behavior with warnings/errors.
Test implicit numeric/string conversion and zero/invalid-date modes explicitly.
Explain ONLY_FULL_GROUP_BY and ORACLE mode as opt-in semantic changes, not universal defaults.
Build a data-quality acceptance test that records mode, charset/collation and representative failing writes.
Every mode change in the mandatory lab is SESSION-scoped and is restored before the session ends. Do not change GLOBAL SQL_MODE on a shared server merely to reproduce an example. Chapter 03 already established the runtime-versus-persistence distinction.
1. SQL_MODE is part of the application runtime contract
MariaDB exposes SQL mode through the
sql_mode system variable. A session inherits the
GLOBAL value when it connects, then can change its own SESSION
value. Stored programs can also retain mode-sensitive creation
semantics. Therefore a migration test must record the mode under
which DDL and queries run; “the server is MariaDB 12.3” is not
enough.
SELECT @@GLOBAL.sql_mode AS global_mode, @@SESSION.sql_mode AS session_mode, @@version AS server_version, @@character_set_connection, @@collation_connection;SELECT REPLACE(@@SESSION.sql_mode, ',', '\n') AS enabled_modes;
Current MariaDB documentation lists a default that includes
STRICT_TRANS_TABLES,
ERROR_FOR_DIVISION_BY_ZERO,
NO_AUTO_CREATE_USER, and
NO_ENGINE_SUBSTITUTION for modern releases. But
deployments can override that globally or per session. Always
test the effective value rather than memorizing the vendor
default.
2. Strictness decides whether bad input becomes an error or coerced data
STRICT_TRANS_TABLES makes invalid or missing values
in transactional tables such as InnoDB produce errors in many
cases where a permissive mode might coerce a value and emit a
warning. “The INSERT returned success” is therefore weak
evidence unless the application also handles warnings and the
expected SQL mode is pinned.
CREATE TABLE servicehub_sandbox.mode_probe ( id INT PRIMARY KEY, small_code VARCHAR(5) NOT NULL, quantity TINYINT UNSIGNED NOT NULL) ENGINE=InnoDB;SET @saved_mode := @@SESSION.sql_mode;SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';-- Expected error: value too long.INSERT INTO servicehub_sandbox.mode_probe VALUES (1,'TOO-LONG',10);SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION';-- Depending on exact statement/type, permissive mode can coerce/truncate and warn.INSERT INTO servicehub_sandbox.mode_probe VALUES (2,'TOO-LONG',10);SHOW WARNINGS;SELECT * FROM servicehub_sandbox.mode_probe WHERE id=2;SET SESSION sql_mode = @saved_mode;
The lab is deliberately evidence-based: inspect both the result
row and SHOW WARNINGS. Do not teach a single
warning text as immutable because wording can change. The
mechanism is what matters: strictness changes whether unsafe
conversion is rejected or tolerated/coerced.
3. Implicit conversion can make syntactically valid SQL semantically dangerous
MariaDB can convert strings to numbers and numbers to strings in expressions. That convenience can surprise indexing, comparison and data-quality logic. For example, comparing a numeric column to a string literal that looks numeric may coerce the string; comparing mixed formats can produce warnings or unexpected matches. Production code should bind parameters with the intended types and avoid using implicit conversion as validation.
SELECT '10' + 5 AS numeric_conversion;SELECT CAST('10' AS UNSIGNED) + 5 AS explicit_conversion;-- Deliberately suspicious input: inspect value and warnings.SELECT CAST('10x' AS UNSIGNED) AS coerced_value;SHOW WARNINGS;
Explicit CAST documents intent but does not make
invalid source data magically correct; it gives you a controlled
place to detect failure/warnings. In ingestion pipelines, stage
raw text first when necessary, validate it, then convert into
typed columns under a known strict mode.
4. Zero and invalid dates are mode-sensitive legacy boundaries
MariaDB has modes such as NO_ZERO_DATE,
NO_ZERO_IN_DATE and
ALLOW_INVALID_DATES that alter acceptance of
unusual date values. Current documentation notes that
NO_ZERO_DATE and NO_ZERO_IN_DATE produce errors in strict mode
and warnings/coercion in less strict paths. The correct modern
application design is not “which zero date should we allow?” but
“does the domain have an unknown date, and if so should that be
NULL or a separate status?”
CREATE TABLE servicehub_sandbox.date_mode_probe ( id INT PRIMARY KEY, scheduled_on DATE NULL) ENGINE=InnoDB;SET @saved_mode := @@SESSION.sql_mode;SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE';-- Expected to be rejected under this strict policy.INSERT INTO servicehub_sandbox.date_mode_probe VALUES (1,'0000-00-00');INSERT INTO servicehub_sandbox.date_mode_probe VALUES (2,'2026-00-15');SET SESSION sql_mode = @saved_mode;
If a legacy migration contains zero dates, do not silently relax production mode and hope. Inventory the values, decide their business meaning, transform them in a controlled staging process, and document any intentional exception.
5. ONLY_FULL_GROUP_BY changes which reporting queries are accepted
ONLY_FULL_GROUP_BY rejects SELECT-list columns that
are neither grouped nor aggregated according to MariaDB’s rules.
It is not part of MariaDB’s long-standing default mode set in
the way many MySQL users expect, so portable applications should
test reports explicitly instead of assuming one vendor’s
defaults.
CREATE TABLE servicehub_sandbox.group_probe ( team_id INT NOT NULL, technician_name VARCHAR(80) NOT NULL, completed_jobs INT NOT NULL) ENGINE=InnoDB;INSERT INTO servicehub_sandbox.group_probe VALUES(1,'Ava',5),(1,'Noah',7),(2,'Mina',4);SET @saved_mode := @@SESSION.sql_mode;SET SESSION sql_mode = CONCAT_WS(',', NULLIF(@@SESSION.sql_mode,''), 'ONLY_FULL_GROUP_BY');-- Intentionally invalid/ambiguous under ONLY_FULL_GROUP_BY:SELECT team_id, technician_name, SUM(completed_jobs)FROM servicehub_sandbox.group_probeGROUP BY team_id;-- Repair: choose an explicit grouped or aggregated result.SELECT team_id, SUM(completed_jobs) AS total_jobsFROM servicehub_sandbox.group_probeGROUP BY team_id;SET SESSION sql_mode = @saved_mode;
The repair is not “turn the mode off.” The repair is to decide what result the query actually means. If you need a technician associated with a maximum, use a deterministic window/subquery pattern in Chapter 05 rather than selecting an arbitrary ungrouped value.
6. ORACLE mode is an explicit compatibility mode—not full Oracle Database
MariaDB supports SQL_MODE=ORACLE. Current
documentation explains that from MariaDB 10.3 it changes more
than a few parser flags: it enables a large subset of
Oracle-style PL/SQL syntax for stored programs and bundles other
compatibility behaviors. That is powerful for migrations, but
“Oracle mode” does not mean complete Oracle Database feature,
optimizer, package, datatype, transaction or operational
equivalence.
SET @saved_mode := @@SESSION.sql_mode;SET SESSION sql_mode = 'ORACLE';SELECT @@SESSION.sql_mode;-- Observe that mode changes parser/compatibility behavior.-- Do not deploy unrelated application sessions under ORACLE mode by accident.SET SESSION sql_mode = @saved_mode;
Treat ORACLE mode like any other compatibility layer: scope it intentionally, inventory the syntax/features it changes, run the real stored-program/SQL corpus, and document unsupported Oracle behaviors. It is a migration aid, not a promise of full product equivalence.
7. Data-quality baseline: mode + charset + collation + time zone
SQL mode does not act alone. Chapter 03 established that time zone, character set and collation also change how input is interpreted and compared. A reproducible application contract should capture these settings together and test representative edge cases whenever a connector, ORM, server series or migration target changes.
SELECT @@version AS server_version, @@SESSION.sql_mode AS sql_mode, @@SESSION.time_zone AS time_zone, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@collation_connection AS collation_connection, @@character_set_results AS character_set_results;
| Acceptance test | Expected evidence |
|---|---|
| Oversized VARCHAR under strict mode | Statement fails; no silently truncated row. |
| Invalid/zero date policy | Behavior matches documented domain decision. |
| Mixed-type conversion | Warnings/errors are detected, not ignored. |
| Grouping query | Deterministic query works with intended ONLY_FULL_GROUP_BY policy. |
| Oracle compatibility test | Only scoped migration sessions use ORACLE mode. |
| New connection | Inherits the documented GLOBAL mode unless application overrides SESSION. |
8. Deliberately wrong approach: make production permissive so imports “work”
Turning off strict modes globally to make a legacy import complete is attractive because the migration stops throwing errors. It also converts unknown data-quality problems into rows you now have to discover later. The safer workflow is to keep the production contract strict, load questionable legacy data into a staging table or disposable instance, capture warnings/rejects, transform invalid values explicitly, then load validated typed rows.
If a legacy application genuinely depends on a permissive mode, document that dependency and isolate it with session/account/application configuration while you plan remediation. A hidden global relaxation affects every new client and can change behavior far beyond the one import that motivated it.
9. Chapter checkpoint lab and cleanup
Run the following acceptance sequence entirely in
servicehub_sandbox. Save your original SESSION mode
and restore it after each experiment.
- Record GLOBAL/SESSION SQL_MODE plus charset/collation/time-zone settings.
-
Run the strict-versus-permissive oversized VARCHAR test and
inspect
SHOW WARNINGS. - Run explicit string-to-number conversion tests and record warnings.
- Test zero/invalid dates under a strict date policy.
- Enable ONLY_FULL_GROUP_BY only for the current session, run the intentionally ambiguous query, then repair it.
- Enter ORACLE mode only long enough to observe the session setting, then restore the saved mode.
- Open a fresh connection and verify which GLOBAL mode it inherits.
-
Drop only the Chapter 04 sandbox objects after checking
DATABASE()and server identity.
Check your understanding
- What is the difference between GLOBAL and SESSION SQL_MODE?
- Why must an application inspect warnings in permissive conversion paths?
- What is the safer representation of an unknown business date than a zero date?
- Why is ONLY_FULL_GROUP_BY useful even when it is not your server default?
- Does SQL_MODE=ORACLE make MariaDB a complete Oracle Database replacement?
Review the answers
GLOBAL SQL_MODE is inherited by new sessions; an existing connection keeps its SESSION value unless it changes it. Permissive conversions can succeed while changing/truncating data, so warnings are part of the evidence. Unknown dates should normally be modeled explicitly—often as NULL plus any needed status—rather than a magic zero date. ONLY_FULL_GROUP_BY exposes ambiguous reporting queries and improves portability discipline. ORACLE mode enables important syntax/compatibility behavior but is not full Oracle Database equivalence.
10. Chapter 04 summary and bridge
Chapter 04 treated data definition as a correctness contract. You learned that MariaDB database/schema naming is one namespace; case policy and temporary shadowing can change object resolution; types encode precision, comparison and time/JSON/UUID/vector semantics; constraints protect invariants; identifier allocators do not promise gaplessness; and SQL_MODE can change whether the same statement is rejected, coerced or parsed differently.
Chapter 05 now assumes this contract is in place and turns to query semantics: NULL, joins, subqueries, common table expressions, recursive traversal, window functions and reporting. The recurring discipline stays the same—write a query, observe its actual result, and explain why MariaDB produced that result rather than relying on syntax familiarity.