{"id":45795,"date":"2026-11-08T04:55:07","date_gmt":"2026-11-08T04:55:07","guid":{"rendered":"https:\/\/www.withdata.com\/blog\/?p=45795"},"modified":"2026-11-10T05:29:03","modified_gmt":"2026-11-10T05:29:03","slug":"visual-foxpro-migration-dbf-cdx-fpt-to-sql-server","status":"publish","type":"post","link":"https:\/\/www.withdata.com\/blog\/dbcopier\/visual-foxpro-migration-dbf-cdx-fpt-to-sql-server.html","title":{"rendered":"Visual FoxPro Migration: DBF, CDX and FPT to SQL Server"},"content":{"rendered":"<p>A FoxPro application rarely dies quietly. It runs for fifteen years, becomes the only system that knows how a business actually works, and then someone needs its data in SQL Server \u2014 for reporting, for a new web app, or because the machine that can still open the forms is about to fail.<\/p>\n<p>The data itself is not the hard part. <strong>A DBF file is a simple, well-documented format.<\/strong> What makes FoxPro migrations fail is everything around it: the four different files that make up a single table, the types that have no direct equivalent, and the ids that quietly change on the way across. This guide covers those, in the order you will hit them.<\/p>\n<hr\/>\n<h2>1. Why FoxPro migrations stall<\/h2>\n<p>Three things go wrong, and none of them is &#8220;the file is corrupted&#8221;:<\/p>\n<ol>\n<li><strong>The table is not one file.<\/strong> A Visual FoxPro table is <code>customers.dbf<\/code> <em>plus<\/em> <code>customers.fpt<\/code><\/li>\n<\/ol>\n<p>(memo and general fields), and often <code>customers.cdx<\/code> (the index). Copy the <code>.dbf<\/code> alone and it looks fine \u2014 until someone opens a notes field and finds it empty.<\/p>\n<ol>\n<li><strong>The values do not survive a naive conversion.<\/strong> <code>Currency<\/code>, <code>DateTime<\/code>, <code>Integer<\/code> and<\/li>\n<\/ol>\n<p><code>Double<\/code> are stored as binary numbers, not text. Any route that converts through CSV or a text dump has to get the precision, the date format and the codepage right on both ends.<\/p>\n<ol>\n<li><strong>Ids and deleted rows change silently.<\/strong> VFP auto-increment values are not SQL Server<\/li>\n<\/ol>\n<p><code>IDENTITY<\/code> values, and a VFP table with 105 records may only expose 100 of them because five are flagged deleted. Both regressions show up <em>after<\/em> the migration is declared finished.<\/p>\n<h2>2. DBC, DBF, CDX, FPT \u2014 what each file actually does<\/h2>\n<table>\n<thead>\n<tr>\n<th>File<\/th>\n<th>Holds<\/th>\n<th>What happens if you ignore it<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>.dbf<\/code><\/td>\n<td>The table: header, field definitions, fixed-length records<\/td>\n<td>The only file people think about; contains everything except memos<\/td>\n<\/tr>\n<tr>\n<td><code>.fpt<\/code><\/td>\n<td>Memo (<code>M<\/code>), General (<code>G<\/code>) and Blob (<code>W<\/code>) field contents, stored out of line<\/td>\n<td>Notes, comments, images and documents disappear \u2014 the record still looks valid<\/td>\n<\/tr>\n<tr>\n<td><code>.cdx<\/code><\/td>\n<td>Compound index: tags, keys, and the index expressions<\/td>\n<td>Queries get slow; worse, an application that relies on <code>SET ORDER<\/code> sees a different row order<\/td>\n<\/tr>\n<tr>\n<td><code>.dbc<\/code><\/td>\n<td>Database container: long field names, validation rules, defaults, triggers, and stored procedures<\/td>\n<td>The long names and rules are lost; you migrate short 10-character names into SQL Server<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The <code>.dbc<\/code> is the one that surprises people in a good way: it is an ordinary table that <em>lists<\/em> the other tables and records the long names. Reading it is the only way to recover a schema that was designed with names like <code>customer_credit_limit<\/code> instead of <code>CRLIMIT<\/code>.<\/p>\n<h2>3. Two ways in: the GUI copier, and scripting the same load<\/h2>\n<h3>Route A \u2014 the GUI copier (what most people actually do)<\/h3>\n<p>Start <a href=\"https:\/\/www.withdata.com\/db-copier\/\" target=\"_blank\" rel=\"noopener noreferrer\">DBCopier<\/a>. The first window is <strong><code>DBCopier Logon<\/code><\/strong>, with two tabs, <strong><code>DB (Source)<\/code><\/strong> and <strong><code>DB (Target)<\/code><\/strong>.<\/p>\n<ol>\n<li>On <strong><code>DB (Source)<\/code><\/strong> set <code>Source Database Type<\/code> to <strong><code>DBF<\/code><\/strong>. The form changes to the DBF fields:<\/li>\n<\/ol>\n<ul>\n<li><strong><code>DBF Path (Folder)<\/code><\/strong> \u2014 a <strong>folder<\/strong>, not a file. Point it at the directory that holds the<\/li>\n<\/ul>\n<p><code>.dbf<\/code>\/<code>.fpt<\/code>\/<code>.cdx<\/code> files; every table in it is offered on the next step.<\/p>\n<ul>\n<li><strong><code>DBF Format<\/code> = <code>Auto<\/code><\/strong> \u2014 let it read the version byte instead of telling it the dialect. This<\/li>\n<\/ul>\n<p>matters: dBase III and VFP write different header sizes, and a tool that assumes one of them fails on the other with <code>Unknown type n<\/code> or <code>Type \"0\" of the field \"\"<\/code>.<\/p>\n<ul>\n<li><strong><code>Code Page<\/code> = <code>Default<\/code><\/strong> \u2014 this is the field that decides whether your text arrives readable.<\/li>\n<\/ul>\n<p>A Chinese, Cyrillic or Greek <code>.dbf<\/code> written under a DOS code page needs the matching code page here; leave it on <code>Default<\/code> and you get the right shape with the wrong characters.<\/p>\n<p>Press <strong><code>Test<\/code><\/strong>; for a folder of <code>.dbf<\/code> files it answers with a single <code>Direct access<\/code> confirmation, and the status line under the buttons goes from <code>Not Connected.<\/code> to <strong><code>Connected.<\/code><\/strong><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.withdata.com\/wp\/wp-content\/uploads\/2026\/11\/dbf-01-logon-source.png\" alt=\"DBCopier Logon, DB (Source): Source Database Type DBF, DBF Path (Folder), DBF Format Auto, Code Page Default, status Connected.\" width=\"586\"><\/p>\n<ol>\n<li>Switch to <strong><code>DB (Target)<\/code><\/strong> and set <code>Target Database Type<\/code> to <strong><code>SQL Server<\/code><\/strong>: <code>Server<\/code><\/li>\n<\/ol>\n<p>(<code>.\\SQLEXPRESS<\/code> for a named instance), <code>Port<\/code>, <code>Database<\/code>. With <code>Authentication<\/code> left on <strong><code>Windows Authentication<\/code><\/strong> the <code>Username<\/code>\/<code>Password<\/code> fields stay empty. <strong><code>Test<\/code><\/strong> answers with the server version, <strong><code>Connect<\/code><\/strong> turns the status line into <code>Connected.<\/code>, and only then does <strong><code>Ok<\/code><\/strong> close the dialog \u2014 press it earlier and it says <code>Please connect to target database<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.withdata.com\/wp\/wp-content\/uploads\/2026\/11\/dbf-02-logon-target.png\" alt=\"DBCopier Logon, DB (Target): Target Database Type SQL Server, Server .\\SQLEXPRESS, Port 1433, Windows Authentication, Database bdv_foxpro, Connected.\" width=\"586\"><\/p>\n<ol>\n<li>The main window now names both sides \u2014 `DBCopier &#8212; Source DBF: <folder>  Target SQL Server:<\/li>\n<\/ol>\n<p><server>:<port>:<database><code> \u2014 and lists the tables it found in the folder. Start the copy from <strong><\/code>Wizard > Table To Table Copy<code><\/strong>; the <\/code>Start a task &#8211; DBCopier<code> dialog offers the same thing as <strong><\/code>Convert From Table`<\/strong>.<\/p>\n<ol>\n<li><strong><code>Open Source<\/code><\/strong> \u2014 the <code>Table<\/code> dropdown lists every <code>.dbf<\/code> in the folder. Picking one fills the grid<\/li>\n<\/ol>\n<p>with the <strong>first 20 rows<\/strong>, which is where the <code>D<\/code>-type dates, the numeric types from \u00a74 and the memo columns can be inspected <em>before<\/em> anything moves.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.withdata.com\/wp\/wp-content\/uploads\/2026\/11\/dbf-03-open-source.png\" alt=\"Open Source: Table DEPARTMENTS, first 20 rows in the grid\" width=\"776\"><\/p>\n<ol>\n<li><strong><code>Set Target<\/code><\/strong> \u2014 <code>Schema<\/code> and <code>Table<\/code> on the SQL Server side, <code>Load Type<\/code>, <code>Save To<\/code> (<code>Database Table<\/code><\/li>\n<\/ol>\n<p>or <code>SQL File<\/code>), <code>Encoding<\/code>, and the <strong><code>Columns Map<\/code><\/strong>: one row per field with <code>Include<\/code>, <code>Source Column<\/code>, <code>Table Field<\/code>, <code>Date Format<\/code> (for the date fields) and <code>NULLIF<\/code>. Fields with the same name are matched for you; <code>Select All<\/code> ticks the ones you keep. If the target table does not exist yet, the button next to <code>Table<\/code> opens <strong><code>Create Table<\/code><\/strong> (<code>Field Type<\/code> = <code>Auto Detect<\/code>), generates the DDL and runs it with <strong><code>Execute<\/code><\/strong>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.withdata.com\/wp\/wp-content\/uploads\/2026\/11\/dbf-04-set-target.png\" alt=\"Set Target: dbo.DEPARTMENTS, Load Type Append, Columns Map with Include ticked for three fields\" width=\"776\"><\/p>\n<ol>\n<li><strong><code>Summary<\/code><\/strong> \u2014 <code>From :<\/code> and <code>To :<\/code> on one line each, the first ten rows as they will arrive,<\/li>\n<\/ol>\n<p><code>Convert Rows<\/code>, <code>Skip Rows<\/code>, <strong><code>Errors to allow<\/code><\/strong> and the <code>Log File<\/code> path.<\/p>\n<ol>\n<li><strong><code>Convert Data<\/code><\/strong> \u2014 it asks <strong><code>Start Converting?<\/code><\/strong>, then reports<\/li>\n<\/ol>\n<p><code>27 records,  Elapsed: 00:00:00.109.  From Table: DEPARTMENTS,  To Table: [dbo].[DEPARTMENTS].<\/code> and closes the log with <code>Total Convert Records: 27<\/code> \/ <code>End Convert<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.withdata.com\/wp\/wp-content\/uploads\/2026\/11\/dbf-05-convert-data.png\" alt=\"Convert Data: 100%, 27 records, Elapsed 00:00:00.109, Total Convert Records: 27\" width=\"776\"><\/p>\n<p>The reason this route wins for FoxPro data is not speed, it is that the memo file is read for you: the <code>.fpt<\/code> blocks are followed by offset and length, so the 4 KB \/ 64 KB \/ 1 MB values in \u00a76 arrive whole, and the target column is created wide enough (<code>varchar(max)<\/code> for a <code>W<\/code>\/<code>M<\/code> field) instead of being truncated at 8,000 bytes.<\/p>\n<h3>Route B \u2014 scripting the same load<\/h3>\n<p>When the copy has to run inside your own pipeline, export from FoxPro to a delimited file and load it with <code>bcp<\/code> or <code>BULK INSERT<\/code>: set <code>MAXERRORS<\/code> or the whole batch dies on the first bad row, pin the date format and the codepage on both ends, and load the memo fields in a <strong>separate pass<\/strong> joined on the primary key \u2014 a delimited export cannot carry them.<\/p>\n<p>For a cutover without downtime, do not read the live table while users are writing to it: copy the files and sync from the copy. Two patterns hold up \u2014 a high-watermark on an auto-increment id or a last-modified column (only if the FoxPro application actually maintains one), or a nightly full copy followed by a row count and a per-table hash, alerting on drift.<\/p>\n<p>Both are scripts you will own and re-test at every schema change; the GUI route above is the same load with the memo offsets and the type mapping already written.<\/p>\n<h2>4. Type mapping: the four fields that need a decision<\/h2>\n<table>\n<thead>\n<tr>\n<th>VFP type<\/th>\n<th>Stored as<\/th>\n<th>SQL Server<\/th>\n<th>Watch for<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>Currency<\/code> (<code>Y<\/code>)<\/td>\n<td>8-byte scaled integer, 4 decimal places<\/td>\n<td><code>money<\/code> or <code>decimal(19,4)<\/code><\/td>\n<td><code>money<\/code> rounds to 4 decimals; <code>decimal<\/code> does not. Pick one and keep it<\/td>\n<\/tr>\n<tr>\n<td><code>DateTime<\/code> (<code>T<\/code>)<\/td>\n<td>8 bytes: Julian day + milliseconds<\/td>\n<td><code>datetime2(3)<\/code><\/td>\n<td><code>datetime<\/code> truncates to 3.33 ms; <code>datetime2<\/code> does not<\/td>\n<\/tr>\n<tr>\n<td><code>Memo<\/code> (<code>M<\/code>)<\/td>\n<td>Block in the <code>.fpt<\/code><\/td>\n<td><code>nvarchar(max)<\/code><\/td>\n<td>The <em>content<\/em> is in another file; the column alone is not enough<\/td>\n<\/tr>\n<tr>\n<td><code>General<\/code> (<code>G<\/code>)<\/td>\n<td>OLE object block in the <code>.fpt<\/code><\/td>\n<td><code>varbinary(max)<\/code><\/td>\n<td>It carries 4\u20138 bytes of OLE header in front of the actual document<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The rest map cleanly: <code>Integer<\/code> (<code>I<\/code>) \u2192 <code>int<\/code>, <code>Double<\/code> (<code>B<\/code>) \u2192 <code>float<\/code>, <code>Logical<\/code> (<code>L<\/code>) \u2192 <code>bit<\/code>, <code>Date<\/code> (<code>D<\/code>) \u2192 <code>date<\/code>, <code>Varchar<\/code> (<code>V<\/code>) \u2192 <code>varchar<\/code>, <code>Blob<\/code> (<code>W<\/code>) \u2192 <code>varbinary(max)<\/code>.<\/p>\n<p>Verify the mapping on one table before doing twenty: check <code>information_schema.columns<\/code> in the target, not the tool&#8217;s summary screen.<\/p>\n<h2>5. Ids, deleted rows and NULL flags<\/h2>\n<ul>\n<li><strong>Auto-increment is a per-table counter, not a sequence.<\/strong> SQL Server will hand out its own<\/li>\n<\/ul>\n<p><code>IDENTITY<\/code> values, so ids can be renumbered \u2014 and any external reference (an invoice number stored in a document archive, a URL, a barcode) breaks. Either keep the original value in an explicit column and enable <code>SET IDENTITY_INSERT<\/code>, or treat the new ids as internal and keep the old ones as a business key.<\/p>\n<ul>\n<li><strong>Deleted rows are still in the file.<\/strong> VFP marks them with a <code>*<\/code> in the first byte of the<\/li>\n<\/ul>\n<p>record; most readers skip them. Decide explicitly whether they should be migrated (usually not), and check the counts against what the application shows, not against the file size.<\/p>\n<ul>\n<li><strong>Nullable fields add a hidden <code>_NullFlags<\/code> column<\/strong> to the <code>.dbf<\/code>. Any tool that reads the raw<\/li>\n<\/ul>\n<p>header has to understand it, or it will trip over a field type it has never seen.<\/p>\n<h2>6. Memo (<code>.fpt<\/code>) and indexes (<code>.cdx<\/code>): what survives<\/h2>\n<p>We tested the memo side rather than guessing, because it is where most migrations quietly lose data. Three values \u2014 4 KB, 64 KB and 1 MB \u2014 were written into an <code>.fpt<\/code> and migrated to SQL Server:<\/p>\n<table>\n<thead>\n<tr>\n<th>Memo size<\/th>\n<th>Landed in SQL Server<\/th>\n<th>Result<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>4 KB<\/td>\n<td>4,085 characters<\/td>\n<td>intact<\/td>\n<\/tr>\n<tr>\n<td>64 KB<\/td>\n<td>65,526 characters<\/td>\n<td>intact<\/td>\n<\/tr>\n<tr>\n<td>1 MB<\/td>\n<td>1,048,568 characters<\/td>\n<td>intact<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The target column was created as <code>varchar(max)<\/code>, so nothing was truncated at the 8,000-byte <code>varchar<\/code> boundary. If your target column is <code>varchar(n)<\/code> or <code>nvarchar(n)<\/code>, that limit will truncate long memos \u2014 check the schema before you load.<\/p>\n<p><strong>Indexes are a different story.<\/strong> <code>.cdx<\/code> files store keys and expressions, not just field names; a tool can rebuild the indexes it recognizes, but it cannot reproduce VFP <code>SET ORDER<\/code> behaviour or index expressions exactly. We did not test index migration in this round, so the honest statement is: assume the index does not travel, and re-create the ones the new application needs.<\/p>\n<h2>7. Target-by-target notes<\/h2>\n<p>The same source table goes to different databases with different consequences:<\/p>\n<table>\n<thead>\n<tr>\n<th>Target<\/th>\n<th>Notes<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>SQL Server<\/strong><\/td>\n<td><code>datetime2<\/code> and <code>varchar(max)<\/code> are the safe landing types; watch the <code>money<\/code> rounding<\/td>\n<\/tr>\n<tr>\n<td><strong>PostgreSQL<\/strong><\/td>\n<td><code>numeric<\/code> and <code>timestamptz<\/code>; the codepage question becomes an <code>ENCODING<\/code> question at <code>CREATE DATABASE<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>MySQL \/ MariaDB<\/strong><\/td>\n<td><code>DATETIME(3)<\/code> and <code>LONGTEXT<\/code>; <code>utf8mb4<\/code> from the start, not after the fact<\/td>\n<\/tr>\n<tr>\n<td><strong>Oracle<\/strong><\/td>\n<td><code>NUMBER(19,4)<\/code> for currency; <code>CLOB<\/code> for memo, which is a locator, not a string \u2014 reads need care<\/td>\n<\/tr>\n<tr>\n<td><strong>DB2<\/strong><\/td>\n<td><code>DECIMAL(19,4)<\/code> and <code>CLOB<\/code>; LOB limits are per-table, check before the load<\/td>\n<\/tr>\n<tr>\n<td><strong>Access<\/strong><\/td>\n<td><code>Memo<\/code> has a 65,535-character limit; the 1 MB case will not fit<\/td>\n<\/tr>\n<tr>\n<td><strong>Teradata<\/strong><\/td>\n<td>Fast for bulk loads, but the <code>SET<\/code> tables have no primary key \u2014 decide how you will deduplicate<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Direct pair pages: <a href=\"https:\/\/www.withdata.com\/tools\/db-to-db\/foxpro-to-postgres\" target=\"_blank\" rel=\"noopener noreferrer\">FoxPro to PostgreSQL<\/a> \u00b7 <a href=\"https:\/\/www.withdata.com\/tools\/db-to-db\/foxpro-to-mysql\" target=\"_blank\" rel=\"noopener noreferrer\">FoxPro to MySQL<\/a> \u00b7 <a href=\"https:\/\/www.withdata.com\/tools\/db-to-db\/foxpro-to-oracle\" target=\"_blank\" rel=\"noopener noreferrer\">FoxPro to Oracle<\/a> \u00b7 <a href=\"https:\/\/www.withdata.com\/tools\/db-to-db\/foxpro-to-db2\" target=\"_blank\" rel=\"noopener noreferrer\">FoxPro to DB2<\/a> \u00b7 <a href=\"https:\/\/www.withdata.com\/tools\/db-to-db\/foxpro-to-access\" target=\"_blank\" rel=\"noopener noreferrer\">FoxPro to Access<\/a> \u00b7 <a href=\"https:\/\/www.withdata.com\/tools\/db-to-db\/foxpro-to-sqlite\" target=\"_blank\" rel=\"noopener noreferrer\">FoxPro to SQLite<\/a>.<\/p>\n<h2>8. How long it takes, measured<\/h2>\n<p>One hundred thousand rows with a realistic mix \u2014 integer id, character name, currency amount, date, and a memo on every thousandth row (2 KB each) \u2014 from a Visual FoxPro <code>.dbf<\/code>\/<code>.fpt<\/code> pair into a local SQL Server instance:<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Rows<\/td>\n<td>100,000<\/td>\n<\/tr>\n<tr>\n<td>Source<\/td>\n<td>DBF + FPT, 12.4 MB of memo data<\/td>\n<\/tr>\n<tr>\n<td>Time<\/td>\n<td><strong>23.2 seconds<\/strong> (wall clock, includes creating the target table)<\/td>\n<\/tr>\n<tr>\n<td>Rows after<\/td>\n<td>100,000, memo contents verified<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That is a laptop-class machine with SQL Server on the same box, so a networked target will add transfer time. The 27-row table in Route A finished in <strong>0.109 s<\/strong> \u2014 that is the other end of the scale, where the number is connection setup and table creation, not rows.<\/p>\n<p><img decoding=\"async\" src=\"\/images\/dbcopier\/foxpro-migration-timing.png\" alt=\"FoxPro to SQL Server: 100,000 rows in 23.2 seconds, and memo values up to 1 MB arriving intact\" style=\"max-width:100%;height:auto;display:block;\" \/><\/p>\n<p>Measure your own tables with the same method: pick one representative table, run it twice (the first run creates the schema), and use the second number. Small tables are dominated by connection setup, not by rows.<\/p>\n<h2>9. Verifying the migration (do this before you switch off the old system)<\/h2>\n<ol>\n<li><strong>Row counts per table<\/strong>, with deleted rows accounted for \u2014 the numbers should match what the<\/li>\n<\/ol>\n<p>application shows, not the raw record count.<\/p>\n<ol>\n<li><strong>A checksum per table<\/strong> over the columns that matter (<code>CHECKSUM_AGG<\/code> in SQL Server, or a<\/li>\n<\/ol>\n<p>hash of the concatenated columns) computed on both sides with the same rules.<\/p>\n<ol>\n<li><strong>A sample of 20 rows read back through the application<\/strong>, including at least one memo, one<\/li>\n<\/ol>\n<p>currency and one nullable field.<\/p>\n<ol>\n<li><strong>Spot-check the extreme memos<\/strong>: the largest <code>.fpt<\/code> value in the table is the one that gets<\/li>\n<\/ol>\n<p>truncated first.<\/p>\n<hr\/>\n<h2>FAQ<\/h2>\n<h3>Is a DBF file the whole table?<\/h3>\n<p>No. The <code>.dbf<\/code> holds fixed-length records; memo and general fields live in the matching <code>.fpt<\/code> file, and indexes live in <code>.cdx<\/code>. Copying only the <code>.dbf<\/code> loses the note fields silently.<\/p>\n<h3>Can I migrate FoxPro data without FoxPro installed?<\/h3>\n<p>Usually yes \u2014 DBF is a documented format and readers exist for it. You need the whole file set (<code>.dbf<\/code> + <code>.fpt<\/code>, and <code>.dbc<\/code> if the database used a container), and the codepage the table was written in.<\/p>\n<h3>Why are some rows missing after migration?<\/h3>\n<p>Most often they were flagged deleted (<code>*<\/code> in the first byte) and the reader skipped them. Compare counts against what the VFP application displays, not against the file length.<\/p>\n<h3>Will the ids stay the same?<\/h3>\n<p>Only if you preserve them deliberately. SQL Server <code>IDENTITY<\/code> generates its own sequence, so keep the original values in a column (and use <code>SET IDENTITY_INSERT<\/code> if the ids must be the primary key).<\/p>\n<h3>How do I handle memo fields that are larger than <code>varchar(max)<\/code>?<\/h3>\n<p><code>varchar(max)<\/code> holds 2 GB, so in practice memo fields fit. The failure mode is a target column defined as <code>varchar(n)<\/code> or an Access <code>Memo<\/code> (65,535 characters) \u2014 check the destination type first.<\/p>\n<h3>Do FoxPro indexes migrate?<\/h3>\n<p>Not reliably. <code>.cdx<\/code> keys and expressions are close to VFP-specific; plan to recreate the indexes the new application needs rather than expecting the old ones to arrive intact.<\/p>\n<h3>What about <code>.dbc<\/code> long field names?<\/h3>\n<p>The <code>.dbc<\/code> is itself a table that records long names, rules and triggers. If the migration ignores it, short 10-character names arrive instead \u2014 usable, but every query and report has to be rewritten.<\/p>\n<h3>Can this be scheduled?<\/h3>\n<p>Yes, both GUI ETL tools and command-line routes can run on a schedule. Do the first load manually and verify it; automate only the repeatable incremental pass.<\/p>\n<hr\/>\n<h2>Which route to pick<\/h2>\n<ul>\n<li><strong>Get the data across this week and keep the app running<\/strong> \u2014 Route A above. No format files, and the<\/li>\n<\/ul>\n<p>preview in <code>Open Source<\/code> lets you check a table before moving twenty of them.<\/p>\n<ul>\n<li><strong>You need the load inside your own pipeline<\/strong> \u2014 Route B, with the <code>bcp<\/code> notes above as the checklist<\/li>\n<\/ul>\n<p>(the date and <code>bit<\/code> conversions are where it bites).<\/p>\n<ul>\n<li><strong>You cannot stop the old application while the data moves<\/strong> \u2014 the second half of Route B: load the bulk<\/li>\n<\/ul>\n<p>once, then sync from a copy until the cutover weekend.<\/p>\n<p>A GUI ETL tool and a command-line route can both run on a schedule; do the first load by hand, check it, then automate the repeats.<\/p>\n<hr\/>\n<h2>Trying it on your own tables<\/h2>\n<blockquote>\n<p><strong>Move a Visual FoxPro table into SQL Server without exporting to CSV or writing a loader first.<\/strong> <a href=\"https:\/\/www.withdata.com\/db-copier\/\" target=\"_blank\" rel=\"noopener noreferrer\">Try DBCopier<\/a> \u2014 Windows \/ macOS \/ Linux, 20+ databases, one-time price.<\/p>\n<\/blockquote>\n<hr\/>\n<p><em>Facts checked 2026-11-08; the Route A walkthrough was re-run and re-photographed 2026-11-10 against SQL Server Express 16.0 on a named instance (<code>Windows Authentication<\/code>). Timings are wall clock on a single machine; memo sizes and row counts are from the test data described in \u00a78. Index migration was not tested and is not claimed.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A FoxPro application rarely dies quietly. It runs for fifteen years, becomes the only system that knows how a business actually works, and then someone needs its data in SQL Server \u2014 for reporting, for a new web app, or because the machine that can still open the forms is about to fail. The data [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2774],"tags":[13770,2318,13846,13844,13845,8695],"class_list":["post-45795","post","type-post","status-publish","format-standard","hentry","category-dbcopier","tag-dbcopier","tag-dbf-to-sql-server","tag-foxpro-database-conversion","tag-foxpro-migration","tag-visual-foxpro-migration","tag-visual-foxpro-to-sql-server"],"_links":{"self":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts\/45795","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/comments?post=45795"}],"version-history":[{"count":8,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts\/45795\/revisions"}],"predecessor-version":[{"id":45857,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/posts\/45795\/revisions\/45857"}],"wp:attachment":[{"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/media?parent=45795"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/categories?post=45795"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.withdata.com\/blog\/wp-json\/wp\/v2\/tags?post=45795"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}