PDF#
Querona reads PDF documents from a File connection. Every .pdf file in the connection’s folders
becomes a row in the built-in builtin.PdfDocuments table, exposing the document’s metadata together with
its extracted text and the tables detected inside it (returned as a JSON array). You can also read PDFs
ad-hoc — without importing metadata — with
OPENROWSET (BULK …) and FORMAT = 'PDF'.
The PdfDocuments table#
Each PDF maps to one row. Alongside the metadata, two columns carry extracted content — Text and Tables — and both are produced only when selected: a query that reads neither runs neither extraction.
Column name |
Column type |
Description |
|---|---|---|
Id |
VarBinary(32) not null |
Blake3 hash of the file content; stable row key |
Path |
NVarChar(4000) null |
OS path to a file |
CreatedOn |
DateTime2 not null |
Document creation date |
LastModifiedOn |
DateTime2 null |
Document last modification date |
Keywords |
NVarChar(1600) null |
Document keywords |
Subject |
NVarChar(300) null |
Document subject |
Title |
NVarChar(300) null |
Document title |
IsEncrypted |
Bit not null |
Encryption flag (1=encrypted, 0=not encrypted) |
NumberOfPages |
Int not null |
Number of pages in document |
Author |
NVarChar(255) null |
Document author |
Version |
Float not null |
PDF specification compatibility version |
SizeBytes |
BigInt not null |
File size in bytes |
EmbeddedFiles |
Int not null |
Document-embedded files count |
Text |
NVarChar(max) null |
Extracted document text (see extraction options below) |
Tables |
NVarChar(max) null |
Detected tables as a JSON array (see extraction options below) |
Text and Tables are nvarchar(max) — Tables holds a JSON array, so shred it with OPENJSON /
JSON_VALUE.
Reading PDFs#
Both ways work on the same File connection:
-- 1) the built-in table (honors the connection's default options)
SELECT Title, Author, NumberOfPages, Text FROM builtin.PdfDocuments;
-- 2) ad-hoc, one or more files, with per-query options
SELECT Path, Text, Tables
FROM OPENROWSET(BULK '*.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF') AS docs;
DATA_SOURCE names the File connection; a file reference is resolved under its directory, and a reference
that escapes it is rejected. See Files and data services for connection setup and folder-to-schema mapping.
Extraction options#
Extraction is steered by options that apply as connection-level defaults to every read (including
builtin.PdfDocuments) and can be overridden per query on an OPENROWSET (BULK ...) read via a small
TOML options table in the FORMATFILE_DATA_SOURCE parameter.
Set the connection-level defaults on the File connection’s edit form — the PDF extraction and PDF text layout sections — in the Querona portal; a query can then override any of them as shown below.
Table extraction#
engine — how tables are detected: auto (default), lattice, or stream.
latticefinds tables drawn with visible ruling lines (cell borders). Use it when the table has a printed grid: it keys off the lines, so it is precise for bordered tables and ignores borderless ones.streaminfers columns from whitespace alignment, for tables with no borders. Use it when columns are held apart only by spacing; it can mis-split dense text or a table that actually has rules.autotrieslatticefirst and falls back tostreamwhen it finds no ruled table. Use it when the layout is unknown or a batch mixes both styles. Its blind spot is a single page holding both a ruled and a borderless table — there, name the engine explicitly or isolate each table witharea.
-- a borderless, whitespace-aligned report: force stream detection
SELECT Tables
FROM OPENROWSET(BULK 'report.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { engine = "stream" }') AS docs;
area — restrict extraction to one rectangle, given as [top, left, bottom, right] in PDF points measured from
the top-left corner and applied to every page in range. Use it when a page mixes a table with other content —
headers, footers, side notes, or several tables — and you want only the region that holds your table: it cleans the
result and stops the detector wandering into non-table text. Omit it to scan the whole page.
-- only the table in the lower half of each page
SELECT Tables
FROM OPENROWSET(BULK 'invoice.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { engine = "lattice", area = [400, 40, 780, 560] }') AS docs;
Text extraction#
These shape the Text column only; they do not affect Tables.
text_mode — the unit the text is emitted in: blocks (default), words, or letters.
blocksgroups words into reading-order paragraphs — the most readable output. Use it for prose you will read, search, or feed to language tooling.wordsreturns every word, space-separated, in extraction order, with no paragraph reconstruction. Use it when you only need tokens (keyword search, term counts), or when a busy layout makesblocksgroup text wrongly.lettersconcatenates the raw glyph stream with no grouping. Use it as a last resort for unusual layouts whereblocksandwordsboth scramble the order, or when you reconstruct the text yourself.
segmenter — how the page is split into blocks; used only by text_mode = blocks: docstrum (default) or
xycut.
docstrumgroups characters by their spacing to nearest neighbours — robust on mixed or irregular layouts, so it is the general-purpose default.xycutsplits the page along its whitespace gaps — cleaner on regular multi-column or grid-like pages, but it can over- or under-split irregular ones. Reach for it whendocstrummerges columns that should stay apart.
reading_order — how the blocks are ordered: unsupervised (default), rendering, or none.
unsupervisedinfers the natural reading order — the right choice for almost every document.renderingkeeps the order in which the PDF draws its content; some files draw in true reading order, others scramble it. Use it whenunsupervisedreorders a document you know is drawn top-to-bottom.noneleaves the blocks in detection order — pick it when you order the text yourself downstream.
dedup_letters — remove duplicated overlapping glyphs; true by default. Bold-by-double-printing and
drop-shadow effects draw a character twice, slightly offset; with dedup on those extra copies are dropped for clean
text. Turn it off only if a document legitimately overlaps distinct characters and you are losing real ones.
margin_left / margin_right / margin_top / margin_bottom — points trimmed from each page edge before
extraction; any text whose box falls inside a trimmed band is dropped. Use them to strip fixed page furniture —
running headers, footers, page numbers, side notes — that sits in a constant edge band, so it neither pollutes the
Text nor leaks into stream tables.
-- words only, and drop a 36-point running header and footer band
SELECT Path, Text
FROM OPENROWSET(BULK 'report.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { text_mode = "words", margin_top = 36, margin_bottom = 36 }') AS docs;
Page selection#
pages — a 1-based range such as "1-3,5" that restricts reading to the listed pages (for both Text and
Tables). Use it to skip covers, contents pages, or appendices, or to target the one page you need; on large
documents it also speeds the read by never touching pages you would discard.
-- only pages 2 through 4
SELECT Path, Text
FROM OPENROWSET(BULK 'manual.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { pages = "2-4" }') AS docs;
Encrypted files#
password — the password that opens an encrypted PDF; required for protected files and ignored for open ones.
Supply it per query. Querona does not store PDF passwords: a connection holds no password field, so the value
lives only for the duration of the statement that carries it.
Note
The password is part of the SQL text, so treat the statement the way you would treat any credential-bearing query — Querona masks the value in its own statement logs, but whatever composes the query (a script, an application, a client’s query history) sees it in full.
SELECT Path, Text
FROM OPENROWSET(BULK 'protected.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { password = "s3cret" }') AS docs;
Multi-file reads#
on_error — what a multi-file (glob) read does when a file cannot be opened: fail (default) or skip.
failaborts the whole read on the first unreadable file. Use it when every file must be processed and a bad one is an error you want surfaced at once.skipreads what it can and passes over the files it cannot open. Use it for bulk reads across a messy corpus, where a few unreadable or wrongly-encrypted files should not sink the batch.
-- read every PDF under 2024/, skipping any that cannot be opened
SELECT Path, Text
FROM OPENROWSET(BULK '2024/**/*.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { on_error = "skip" }') AS docs;
Examples#
-- an encrypted document, every PDF under 2024/, skipping any that cannot be read
SELECT Path, Text
FROM OPENROWSET(BULK '2024/**/*.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF',
FORMATFILE_DATA_SOURCE = 'options = { password = "s3cret", on_error = "skip" }') AS docs;
-- shred the detected tables (Tables is a JSON array)
SELECT d.Path, t.[key] AS table_index, t.[value] AS one_table
FROM OPENROWSET(BULK 'invoice.pdf', DATA_SOURCE = 'files', FORMAT = 'PDF') AS d
CROSS APPLY OPENJSON(d.Tables) AS t;
See also#
Files and data services — the File provider: connections, folder-to-schema mapping, and the other file types
OPENROWSET — the full
OPENROWSET (BULK ...)reference