-- 81-error-paths.sql
--
-- Regression signals for the *error-message* contract of the UDF
-- surface — distinct from `80-unsupported-shapes.sql` which locks
-- the failure-mode of SPARQL translator gaps. This file pins the
-- stable error prefix each UDF emits when given an invalid input
-- (missing file, bad graph id, malformed Turtle, etc.). Downstream
-- callers (CloudNativePG operators, client libraries, CI scripts)
-- match on these prefixes; a silent rename would break them
-- without any pgRDF-side test firing.
--
-- Out of scope: the exact tail of SQLERRM — OS-level `os error N`
-- numbers, parser line:col coordinates, `std::io::Error` Display
-- strings — all of which are platform/build/locale dependent. Only
-- the stable Rust-side error prefix our UDF emits IS pinned.
--
-- Like `80`, each check is wrapped in plpgsql `BEGIN ... EXCEPTION
-- ... END;` so the captured baseline is a clean boolean (`t` =
-- expected substring present in SQLERRM); the volatile tail isn't.
--
-- One check per commit. This file accretes locked rows as the
-- 66 → 1 countdown toward v0.3.0 progresses:
--   error-66 load_turtle missing file    → 'load_turtle: failed to open'
--   error-65 load_turtle invalid base IRI → 'load_turtle: invalid base IRI'
--   error-64 parse_turtle malformed input → 'load_turtle: turtle parse error'
--   error-63 sparql malformed query       → 'sparql: parse error:'

DROP EXTENSION IF EXISTS pgrdf CASCADE;
CREATE EXTENSION pgrdf;
SELECT pgrdf.shmem_reset();

-- The check helper:
--   * Runs `sql` inside a try/catch via EXECUTE (so it works for
--     arbitrary statements, not just `pgrdf.sparql()` SETOF queries
--     like `_check_gap` in `80`).
--   * If the statement SUCCEEDED, emits `unexpected success` (which
--     fires the diff against the baseline).
--   * If the statement failed AND SQLERRM contains the stable
--     `expected_fragment` substring, emits `t`.
--   * If the statement failed but the SQLERRM message changed shape,
--     emits `f` plus the new SQLERRM (so the diff carries
--     diagnostics).
CREATE OR REPLACE FUNCTION _check_error(label TEXT, sql TEXT, expected_fragment TEXT)
RETURNS TEXT
LANGUAGE plpgsql AS $$
DECLARE
  msg TEXT;
BEGIN
  BEGIN
    EXECUTE sql;
    RETURN format('%s: !!! unexpected success !!!', label);
  EXCEPTION WHEN OTHERS THEN
    msg := SQLERRM;
  END;
  IF position(expected_fragment IN msg) > 0 THEN
    RETURN format('%s: t', label);
  ELSE
    RETURN format('%s: f (got: %s)', label, left(msg, 80));
  END IF;
END
$$;

-- ─── Error 66: load_turtle missing file ──────────────────────────
-- `src/storage/loader.rs::load_turtle` opens the path with
-- `File::open(path)`. The `unwrap_or_else` panics with the literal
-- prefix `load_turtle: failed to open` followed by the path and the
-- underlying `std::io::Error` Display. Downstream tooling matches
-- the prefix to decide whether to retry / surface to the operator;
-- the tail is informational only.
SELECT _check_error(
  'error-66 load_turtle missing file',
  'SELECT * FROM pgrdf.load_turtle(''/nonexistent/path/i/promise.ttl'', 9981);',
  'load_turtle: failed to open'
);

-- ─── Error 65: load_turtle invalid base IRI ──────────────────────
-- `src/storage/loader.rs::ingest_turtle_with_stats` (shared by both
-- `load_turtle` and `parse_turtle`) calls `parser.with_base_iri(base)`
-- and `unwrap_or_else` panics with the literal prefix
-- `load_turtle: invalid base IRI` followed by the bad value Debug-
-- formatted and the underlying oxiri Display. The prefix says
-- `load_turtle:` regardless of which UDF triggered it — that name
-- IS the locked contract surface; the tail (oxiri's specific
-- complaint, e.g. `Invalid IRI code point ' '`) is informational.
--
-- We trigger via `parse_turtle` (not `load_turtle`) so the test does
-- not depend on a fixture file; the panic prefix is identical
-- because both UDFs route through the same ingest function.
-- Empty-string base_iri is filtered out before `with_base_iri` is
-- called (see `loader.rs` `.filter(|s| !s.is_empty())`), so the bad
-- value must be syntactically invalid yet non-empty —
-- `'not an iri at all'` (whitespace + no scheme) fits.
SELECT _check_error(
  'error-65 load_turtle invalid base IRI',
  $$SELECT pgrdf.parse_turtle('@prefix ex: <http://e/> . ex:a ex:b ex:c .', 9982, 'not an iri at all')$$,
  'load_turtle: invalid base IRI'
);

-- ─── Error 64: parse_turtle malformed input ──────────────────────
-- `src/storage/loader.rs::ingest_turtle_with_stats` iterates the
-- oxttl parser with `for triple_result in parser { triple_result
-- .expect("load_turtle: turtle parse error") }`. Any syntactically
-- invalid Turtle — missing trailing dot, undeclared prefix, bad
-- IRI ref, RDF-star in default mode, etc. — surfaces an oxttl
-- `Err(...)` that `.expect()` panics through with the literal
-- prefix `load_turtle: turtle parse error` followed by the oxttl
-- error Display (line:col coordinates, structural shape, etc.).
-- The prefix says `load_turtle:` regardless of whether the caller
-- entered through `pgrdf.load_turtle()` or `pgrdf.parse_turtle()`
-- — the cross-UDF prefix invariance (locked in error-65) extends
-- here too: downstream tooling routes on one substring whichever
-- UDF parsed the bytes.
--
-- We fire via `parse_turtle` so the regression has no file fixture.
-- The fragment `':alice :name "Alice"'` uses the default `:`
-- prefix without declaring it; oxttl rejects at byte 0 with
-- `The prefix : has not been declared`. Any of the malformed
-- variants would trip the same prefix; we lock the prefix, not
-- the specific oxttl complaint (which is tail / volatile).
SELECT _check_error(
  'error-64 parse_turtle malformed input',
  $$SELECT pgrdf.parse_turtle(':alice :name "Alice"', 9964)$$,
  'load_turtle: turtle parse error'
);

-- ─── Error 63: sparql malformed query ────────────────────────────
-- `src/query/executor.rs::translate_query` (the entry the user-
-- facing `pgrdf.sparql()` SETOF UDF dispatches into) parses the
-- query string with `SparqlParser::new().parse_query(sql)` and
-- `unwrap_or_else` panics with the literal prefix
-- `sparql: parse error:` followed by the spargebra `ParseError`
-- Display (which carries `error at L:C: expected …` coordinates).
-- The prefix is the user-facing contract surface — operators and
-- client libraries scrape SQLERRM on `sparql:` to distinguish
-- query-parse failure from translator gaps (`80-unsupported-
-- shapes`) and from RDF ingest failures (`66/65/64`).
--
-- The sibling UDF `pgrdf.sparql_parse(query)` (introspection
-- helper) routes through its own panic site with prefix
-- `sparql_parse:` instead — a deliberate distinction so callers
-- can tell which entry point the bytes came in through. We lock
-- only the `pgrdf.sparql()` path here; the `sparql_parse` prefix
-- is a separate contract surface (covered by the `#[pg_test]`
-- `sparql_parse_syntax_error_panics` in `src/query/parser.rs`).
--
-- We lock the prefix only — the tail (spargebra's line:col
-- coordinates, the specific token it expected, e.g.
-- `expected CONSTRUCT`) is volatile across spargebra versions and
-- is informational, not contractual.
SELECT _check_error(
  'error-63 sparql malformed query',
  $$SELECT * FROM pgrdf.sparql('this is not sparql at all')$$,
  'sparql: parse error:'
);

DROP FUNCTION _check_error(TEXT, TEXT, TEXT);

-- Cleanup
DROP EXTENSION pgrdf CASCADE;
CREATE EXTENSION pgrdf;
SELECT pgrdf.shmem_reset();
SELECT pgrdf.plan_cache_clear();
