TweetFollow Us on Twitter

Tightwads Flat File db
Volume Number:12
Issue Number:12
Column Tag:Tools Of The Trade

A Tightwad’s Guide to Flat File Databases

Working with relations in a flat file data base engine

by Edward Ringel, Waterville, Maine

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

By Way of Introduction

One of the most naive, chutzpah-riddled tasks I ever set for myself was trying to build an accounts receivable program for my medical practice in my spare time after reading the Omnis 3 manual and tutorial. After spending a lot of time (and obviously not an inconsiderable sum on this program) I gave up, got wise and bought Tess 2.0, which we’ve used happily through to the present version of 4.4. It is interesting that this fiasco propelled me 1) to program in a language, rather than a database, so that I had complete control of the environment and 2) to think a bit about databases and database design.

Programming to solve database problems using a general purpose computer language inevitably turns one’s attention to database engines and source code libraries to lighten the programming load. While the ads for many of these products promise all kinds of relational muscle, a lot of serious programming tasks don’t need that kind of horsepower. In the course of completing several programming tasks of interest to me, I’ve developed some techniques for using “less powerful” flat file database engines that may be of general interest. For some of you, this may be fairly elementary; for others enlightening. Please bear with me.

Tools and Questions

When I try to solve any problem, I try to conceive the question as carefully as possible and then look at the tools available that might be useful. For the purpose of this discussion, let’s alter the normal thought process and start with the tools. The high priced libraries are expensive for several good reasons.

These database engines are powerful. They can handle thousands, if not millions of records. They can do wonderfully fast searches. They are optimized to read and write keys and data as fast as the OS will permit. They do extensive error checking and error correction. They permit the arbitrary changing of record size, key size, etc. They permit the creation of relational tables and thus the creation of relational databases. Some interface with object oriented paradigms.

Does Your Problem Really Need a Tool This Powerful?

Most of us write databases that handle hundreds or thousands of records, not millions. Most of us write programs where very fast search, read, and write functions are not critical because there is a user sitting there who can’t type that fast. I’m old fashioned about error checking; I don’t feel as though I can ever turn down responsibility for this aspect of programming. Good planning can obviate the need for after-the-fact changes to database structure, and even better planning can allow for it. Relational tables, while useful, are expensive in terms of efficiency and disk space, and besides 1) a lot of really important information in our lives is flat in structure, and 2) the data can be represented and interrelated with a simpler scheme than a relational table. Object persistence is neat, and I don’t know how to write an object persistence scheme; for all I know maybe you can’t come close to NeoAccess or its cousins with what I’m going to propose. Maybe I’m way off base. However, nothing in my thinking precludes a database of objects where constructor/initializer routines reestablish object relationships as the objects are loaded.

Why a Flat File Database Library?

It is time inefficient, error prone, and hard to write a set of routines that manages keys, records, and files. While it may be hard to justify the cost of some of the high end products, it is equally hard to justify the enormous investment of time and hassle in writing your own set of database routines. A flat file data base engine is an excellent compromise between cost and features. This tool is reasonably efficient, and is within most programmers’ budgets. It supplies you with the code that is the most difficult to write. There will still be an investment of time and energy that will be necessary, but it won’t be nearly as dear as starting from scratch. This kind of library has three main features:

• A scheme for record allocation, deletion, and reuse of deleted record file space.

• A record indexing scheme. This is usually some sort of b-tree, but can be a hash table or some other indexing system.

• Extensive error checking during record and key operations, and result codes that indicate 1) whether the database engine did what you asked it to do and 2) if there was an operating system I/O error.

These basic functions can be used very successfully to manage relatively complex data sets. I am going to assume that anyone reading this article understands the basic concepts of record and key operations.

I use a library called B-Tree Helper, from (m)agreeable software in Plymouth, Minnesota ( The current version is 2.2. This product comes as C source code. The function calls are straightforward. Mel Magree is delightfully old fashioned in that he actually includes a hard copy manual for no extra charge! The product can handle fixed and variable length records, many indices, and is very Mac-like in that a single physical file on disk can contain multiple index and record types; no separate index and record file is needed for each logical grouping of data (unless you wish to do so.)

Some of the earlier incarnations of B-Tree Helper had some clumsy function calls and too many pointers to too many temporary results. The current version is very nice and well worth the upgrade if you are an owner of an older version. This product is used for the demonstration application. The source code provided for the demo will compile only if you supply the .c files to B-Tree Helper; the .h files for the library are provided. B-Tree Helper is a robust, good product and has met my needs nicely. There are other flat file engines available, but this article is not a product review. The reader can easily find and compare different products.

Invoicing Revisited

To make my points, and to demonstrate some techniques, I would like to revisit the model problem of invoicing. This is a standard database problem that some of you may have encountered previously in examples or classes. The classical invoicing structure, at its simplest, contains four different types of records, each contained within its own file. Figure 1 shows the basic interrelationships of data.

Figure 1.

There is a customer, a part, an invoice, and a line item. The customer record contains data about the customer, how much he/she owes, etc. The parts record contains information about each item being sold. The invoice is an open-ended structure that tells us about who the customer is, and the various items sold to the customer at that interaction. Each line item consists of the part, the number of parts sold, the price, etc.

When there is a sale, the program would create an invoice, n number of line items, and relate the invoice and line items to pre-existing records that represented customer and part information. Each line item structure contains a reference to the part information for that line item. This can be done either through a relational table, or it may be done simply by embedding a reference to the part information in the line item. When a line item record is retrieved, the application is written so that the information about the part reference is retrieved.

I solve this problem without a relational database. The customer does not need to have a direct relation to a line item; the invoice does that for him/her. Similarly, the customer needs no relation to the parts catalog. With some planning, and understanding of the information problem, a series of special indices and embedded links can be constructed that give you all the data interrelations that you will need. In any setting where the interrelationships are fixed, and the data queries are predictable and have been anticipated, a combination of careful programming and good data structures are a powerful data management system.

Don’t Scare Your Customers - Think Ahead

This kind of programming requires a lot of forethought. Unlike an interface, it is hard to create data structures on the fly. It is critical to have a clear vision of the problem and how you are going to solve it, because you are hard-coding data relationships. It will be difficult for you, and frightening for your user (yes, frightening) when you twiddle an existing file to “add some stuff.” Why frightening? I’ll use myself as an example. My accounts receivable program handles billing and payments from insurance companies and from patients. I literally eat and pay my bills from the information in this file. Tess 4.4 and its programmer(s) are great.

However, what if this alert came up on the screen when I installed an upgrade? “Make sure you have three clean backup copies of your current data because we’re going to twiddle your file structure and add some stuff we forgot. We’re going to completely tear apart your file structure and rebuild it, from scratch, on the fly. Don’t do this operation during a thunderstorm. In fact, don’t even breath near the computer for the next couple of hours. This will take half an hour per megabyte of data on a Power Mac 9500. Press OK to continue.”

You and your users will need to live with your decisions for a long time. It may be worthwhile thinking more like a civil engineer in a litigious society than a software engineer trying to be elegant: overbuild, overbuild, overbuild. Talk to your end user(s). Know the problem before you try to solve it. Build in extensibility. We will see how to do that in a few paragraphs.

Invoice Example and B-Tree Helper in Detail

My example program, which is cleverly named Invoice Example, and B-Tree Helper deserve some explanation. Invoice Example is based loosely on some sample code from DTS, which I used as a framework to handle three modeless dialogs, a couple of menus, a bunch of alerts and a single modal dialog. The interface is not elegant, but it gets the job done. I have separated the code into GUI related files and function related files. The user interface code is fairly pedestrian and I have no secret techniques; the meat of this article is in the folder called “functional files.” Header files for B-Tree Helper are included, but obviously not the source files. A compiled 68K application with an example data file is available for download and on the subscription disk. The source code and the application genuinely complement this article. However, some of the important routines are long and are not well suited to hard copy presentation. Please review the source code (obtained from disk or ‘net) or many of the points may be murky.

To perform an operation, the user selects from a popup menu in the window. This sets the mode (find, next, insert, edit, etc.). When all of the data is ready, the user clicks the Do It button. In the Invoice window, two steps are required for insertion. First the user enters references to customer and parts and then clicks the Load Related button. This checks for valid data, does some math, and then loads the related records. Second, the user clicks the Do It button. Searches and deletions of invoices are simpler and require only a mode selection and a Do It click. There is a window each for Customer, Invoice, and Part operations. The fourth window is for a separate example which we will get to later.

The program reads and writes data to a file, and then uses embedded information or appropriate keys and indices to recall related information. The precise mechanism for this is in the commented sample code. We use B-Tree Helper for all file related functions with the exception of some resource calls.

B-Tree Helper consists of 40 calls that create, open and close files, insert and delete records, insert and delete keys, do complex searches, manage record buffering, add and delete index trees, and permit retrieval of raw page data for debugging. To create a file, the main programming task is the initialization of a series of index trees, which are then passed to the creation routine. These trees represent your index structures.

The other major planning task is to determine file block size (space in the file is allocated by blocks). The size of the block determines the maximum record size, because B-Tree Helper uses control records interspersed with your data to manage available file space. These control records are immovable islands in your data stream, and your records must fit between them. Thus, you must calculate your allocation block size so that your maximum sized data can fit between the control records. Although B-Tree Helper allows variable length records, you will hit a size ceiling of 8*fileBlockSize*(fileBlockSize-8) bytes unless you split your data into chunks (which is not the end of the world either). Upon successfully creating a file, a FileControlHandle - the record that manages the file while open - is returned and is used in virtually every other call to the B-Tree Helper library.

Interestingly, B-Tree Helper does not require formal definition of a record; you simply request n bytes of data from the file. If successful in allocating this block of file space, you are given a success result code and a file address. You can then write and subsequently read data from this address. This address is a four byte long and is also passed to key insertion functions. When a key is searched for and found, the key is returned and so is the file address. However, it is not required to pass a file address to the key. Any four byte value is legal, and this can be used very much to our advantage, as will be shown later. Because records are not defined, keys are also not irretrievably linked to a record type or logical file. This can also be used to our advantage. Finally, because a record is not specifically defined as to content or size, many kinds of records can coexist in a single file. Thus, I think of a B-Tree Helper file as a physical file enclosing any number of logical files, index structures, and map control records (Figure 2) B-Tree Helper is very flexible in this manner.

Figure 2.

Reduce Flexibility Creatively

When I have tackled more involved problems than Invoice Example, I have taken the time to reduce flexibility and create structures that describe each index and record type. This has saved me an enormous amount of headache when debugging and writing code.

Review of the B-Tree Helper functions shows that there is great similarity among the calls of a given function class; key related functions and record related functions all pretty much take the same kind of parameters. I have usually written parameter block structs that I then pass to a custom function that is a wrapper for the B-Tree Helper call. This relieves me of getting the parameters and degree of indirection right each time I call Insert_Key() or Find_Equal(). Additionally, I declare these parameter block structures as global or static. Just as each logical file in your database has an “active” or “current” record (i.e. a record that is loaded into memory and being operated upon), there is a corresponding active key and corresponding active file address. Although I usually separate the actual data from the parameter block, having current keys and current file addresses that can survive between function calls can be very useful, especially when doing successive operations upon multiple logical files. This is demonstrated by supporting Next and Previous functions in the example program.

I am not heavily into object oriented programming, but writing a series of wrapper classes for B-Tree Helper seems like a very sensible, productive thing for you C++ buffs out there to do. In particular, C++’s multiple inheritance would permit creating complex objects that could respond to a single command to insert a record, a key, and then link to a related record.

Making Relationships Work

Records with embedded addresses

I have repeatedly referred to linking to related records. How do we do this? I use three basic techniques.

First embed a file address into a record. Let’s examine the declaration for the CustomerType struct:

struct CustomerType{
 unsigned char   CustName[32];
 long   CustNo;
 long   StringAddr;
typedef struct CustomerType CustomerType;
 unsigned char   gCustString[40];
 CustomerType  gCustomer;

I thought CustomerType had the information we need for the example; however I “forgot” to allow room for a comment. Luckily, I had thought ahead and had left an extra four byte field in CustomerType, which I then renamed StringAddr. After we retrieve all the information from the dialog, we Get_Bytes() for gCustString first, and Write_Data() from the memory address of gCustString to the file address returned by Get_Bytes(). We then set gCustomer.StringAddr to the file address returned by Get_Bytes() as well. Then, and only then, do we allocate space and write the data for the parent gCustomer record.

When information is retrieved, we follow the reverse sequence. We Read_Data() and get gCustomer first. We then Read_Data() using the file address stored in gCustomer.StringAddr to get the child gCustString.

I use this technique when the referenced record is truly a child record - it will never be looked for unto itself. This is a nice way of gracefully adding to a record which was missdeclared, and is a prime way of adding record extensibility. For this reason, I will generally add an extra unused four byte field to just about any record I declare in a “serious” program. This is one of the simplest ways of overbuilding your software to keep data integrity at a maximum. Had we used this scheme and needed to make an ex post facto change to an existing file, my update routine would have read into memory the parent record, allocated and wrote the child record (the new stuff), set the correct field of the parent record with the file address of the child record, and rewrote the parent record to file. No reindexing or reconstitution of existing data would have been necessary.

Sequence numbers to the rescue

This is a nice, simple system. It works well. Why limit its use to a strict parent/child relationship? The problem is that of editing or updating data. If gCustString were a record that had multiple links, and the file address of gCustString’s data were to change (which can easily happen when a record is edited or updated), I would need to find every record that had a link to the changed record and insert the new file address. There are two remedies to this problem. One is to create a pseudo file address that remains constant regardless of the true file address of the record, but always points to the true file address (like a handle). This strategy is used by DynaBase, a flat file database engine which is no longer commercially available. In DynaBase, all file addresses were pseudo addresses, adding overhead where none was needed.

The second approach is to use a sequence number; at allocation each record is assigned a number which is that record’s and only that record’s, never to change for the life of the file. There are a number of ways to issue sequence numbers. For the demo, I used a custom resource that is a handle to a small record whose fields are incremented whenever a record is allocated. This technique is shown in the example program and I will not dwell on it here. It is equally easy to put a sequence number dispatching record in the data fork; using B-Tree Helper to make a Get_Bytes() call for your sequence dispatching record and put the resulting file address into one of the .application1..4 fields of the FileControlHandle and the address of the record will survive across work sessions. See the Picture Creator demo for a simple example of this technique.

If we examine the declaration for the invoice struct, we see that the line item fields are in fact long integers:

struct InvoiceType {
 long   InvoiceNumber;
 long   LineItem1;
 long   LineItem2;
 long   CustNo;

typedef struct InvoiceType InvoiceType;
InvoiceType gInvoice;

After each line item record has been filled in by the user, the program goes to work. A sequence number is issued to that line item record, and the appropriate field in the line item record is filled in with that sequence number. Second, that sequence number is inserted into field LineItem1 or LineItem2. Third, a key is inserted in an index tree, with the sequence number as the key pointing to the file address of the line item record.

This second way of relating a record, that is, to embed the sequence number of a record into the body of a related record, is shown in Figure 3.

Figure 3.

When an invoice is retrieved, the invoice record is retrieved first. A search is performed on the sequence number in either or both of the two line item fields in the invoice record. The file address that is retrieved is the file address of the line item to be read into memory, and a Read_Data() is performed.

This example is trivial, and each line item record belongs to one and only one invoice. Correcting the invoice record for a change in file address of the line item would be easy even if a sequence number had not been issued. However, more complex data structures may have a record that links to many other records, and each record would have to be brought into memory, fixed, and rewritten to disk. That’s a lot of error prone work. With B-Tree Helper, there is a slick function called Change_Address(), which will fix the address of a key when there is a file address change. Even if Change_Address() did not exist, this is only a few lines of code.

Indirect Index Trees

The final way to relate records is with the use of a key from one record pointing to the sequence number of a related record (Figure 4).

Figure 4.

One of the functions we have in Invoice Example is to find the invoices belonging to a customer. Using the name of the customer as the key, we insert this key pointing not to a file address, but to a long that is the sequence number for an invoice belonging to the customer - B-Tree Helper will accept any long for the “file address” portion of the key. We then use this retrieved sequence number as the key to another index where the sequence number key now points to a real file address. Thus, two key lookups must be performed for this search, but the relationship between the customer record and the invoice record will survive file address changes of the invoice nicely if just the second key is kept updated. This kind of relationship is very useful when a record may have a relation to many other relations.

For so-called “many-to-many” relationships, a series of intermediary relating records, consisting of just sequence numbers with appropriately directed keys, can fill the bill. Implementation of this solution is left as an exercise for the reader, which translated into plain English means that you may want to rethink buying one of the more complete solutions if your problem is very hairy. Just remember though, the invoice record that we created is in fact the very kind of record with pointers to other records in other files. Maybe it isn’t as hard as you thought.

General Data Management with a Database Library

The last idea on the creative use of flat file database engines has nothing to do with databases, just plain information management. Although some of this discourse may be rendered obsolete by OpenDoc, consider the problem of the complex document. Several years ago I wrote a program that did extensive data crunching of information on patients’ sleep. There were half a dozen header records of fixed length, and 7 arrays that could vary anywhere between a couple of hundred and several thousand bytes a piece, depending on the severity of the sleep disturbance. Rather than reinvent the wheel to save this complex document to disk, I simply used a database manager. I viewed each piece of information as a variable length record within a logical file. I let the database do all the insertions, deletions, and updates for me, rather than writing routines from scratch. With the arrays varying so greatly in size (I saved the arrays as a single block, rather than element by element), it was much easier to use generic pre-built routines that handled variable length records.

As an example of this solution, I have made a fourth dialog window that opens a picture of Joshua Chamberlain (one of our most famous Mainers) and a short text blurb about him. While this could have been saved as a couple of resources (and in fact the example file was created from resources), the picture and text are in the data fork of a B-Tree Helper file. This is one of the most intriguing uses of a database engine, particularly for the independent, small project programmer.

My last point to make has to do with error checking. Do it. Do it a lot. My example doesn’t do it half enough. The only specific advice I have is to insert data first and keys second. It is much worse to have a key pointing to nowhere than an orphan record, and it makes the cleanup procedure after a failed read/write much easier.

Punch Line

To summarize, here are the salient points of this article:

• Not every database problem needs a relational database.

• Use a flexible flat file database manager as your major tool for data management.

• Consider and construct your data relations very carefully.

• Add an extra four byte field or two to your record declarations. They may come in handy later.

• Consider writing custom functions around the database toolbox calls to improve readability, reduce errors during programming, and make file management easier. If working in an object oriented environment, consider building a series of good quality wrapper classes for the function/parameter block entities that can be reused.

• For simple parent-child data relations, simply embed the address of the child in the parent record.

• Use embedded sequence numbers for relatively simple relations where the child record may be moved within the file.

• Use keys from one record pointing the sequence number of another record to create complex data relations.

• Consider using database engines to manage complex documents.

• Error check till you can’t stand the sight of an if (myErr != noErr) clause.

May you all be spared the ignominy of my first project.


Community Search:
MacTech Search:

Software Updates via MacUpdate

Google Earth - View and contr...
Google Earth gives you a wealth of imagery and geographic information. Explore destinations like Maui and Paris, or browse content from Wikipedia, National Geographic, and more. Google Earth combines... Read more
QuickBooks R12 - Financial...
QuickBooks helps you manage your business easily and efficiently. Organize your finances all in one place, track money going in and out of your business, and spot areas where you can save. Built for... Read more
Google Earth - View and contr...
Google Earth gives you a wealth of imagery and geographic information. Explore destinations like Maui and Paris, or browse content from Wikipedia, National Geographic, and more. Google Earth combines... Read more
QuickBooks R12 - Financial...
QuickBooks helps you manage your business easily and efficiently. Organize your finances all in one place, track money going in and out of your business, and spot areas where you can save. Built for... Read more
FileZilla 3.24.0 - Fast and reliable FTP...
FileZilla (ported from Windows) is a fast and reliable FTP client and server with lots of useful features and an intuitive interface. Version 3.24.0: New The context menu for remote file search... Read more
Bookends 12.7.8 - Reference management a...
Bookends is a full-featured bibliography/reference and information-management system for students and professionals. Bookends uses the cloud to sync reference libraries on all the Macs you use.... Read more
Duplicate Annihilator 5.8.3 - Find and d...
Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes. Duplicate Annihilator detects... Read more
BusyContacts 1.1.6 - Fast, efficient con...
BusyContacts is a contact manager for OS X that makes creating, finding, and managing contacts faster and more efficient. It brings to contact management the same power, flexibility, and sharing... Read more
MarsEdit 3.7.10 - Quick and convenient b...
MarsEdit is a blog editor for OS X that makes editing your blog like writing email, with spell-checking, drafts, multiple windows, and even AppleScript support. It works with with most blog services... Read more
BusyCal 3.1.4 - Powerful calendar app wi...
BusyCal is an award-winning desktop calendar that combines personal productivity features for individuals with powerful calendar sharing capabilities for families and workgroups. Its unique features... Read more

Super Mario Run dashes onto Android in M...
Super Mario Run was one of the biggest mobile launches in 2016 before it was met with a lukewarm response by many. While the game itself plays a treat, it's pretty hard to swallow the steep price for the full game. With that said, Android users... | Read more »
WarFriends Beginner's Guide: How to...
Chillingo's new game, WarFriends, is finally available world wide, and so far it's a refreshing change from common mobile game trends. The game's a mix of tower defense, third person shooter, and collectible card game. There's a lot to unpack here... | Read more »
Super Gridland (Entertainment)
Super Gridland 1.0 Device: iOS Universal Category: Entertainment Price: $1.99, Version: 1.0 (iTunes) Description: Match. Build. Survive. "exquisitely tuned" - Rock Paper Shotgun No in-app purches, and no ads! | Read more »
Red's Kingdom (Games)
Red's Kingdom 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Mad King Mac has kidnapped your father and stolen your golden nut! Solve puzzles and battle goons as you explore and battle your... | Read more »
Turbo League Guide: How to tame the cont...
| Read more »
Fire Emblem: Heroes coming to Google Pla...
Nintendo gave us our first look at Fire Emblem: Heroes, the upcoming mobile Fire Emblem game the company hinted at last year. Revealed at the Fire Emblem Direct event held today, the game will condense the series' tactical RPG combat into bite-... | Read more »
ReSlice (Music)
ReSlice 1.0 Device: iOS Universal Category: Music Price: $9.99, Version: 1.0 (iTunes) Description: Audio Slice Machine Slice your audio samples with ReSlice and create flexible musical atoms which can be triggered by MIDI notes or... | Read more »
Stickman Surfer rides in with the tide t...
Stickson is back and this time he's taken up yet another extreme sport - surfing. Stickman Surfer is out this Thursday on both iOS and Android, so if you've been following the other Stickman adventures, you might be interested in picking this one... | Read more »
Z-Exemplar (Games)
Z-Exemplar 1.4 Device: iOS Universal Category: Games Price: $3.99, Version: 1.4 (iTunes) Description: | Read more »
5 dastardly difficult roguelikes like th...
Edmund McMillen's popular roguelike creation The Binding of Isaac: Rebirth has finally crawled onto mobile devices. It's a grotesque dual-stick shooter that tosses you into an endless, procedurally generated basement as you, the pitiable Isaac,... | Read more »

Price Scanner via

Twelve South Releases RelaxedLeather Cases fo...
Inspired by the laid-back luxury of burnished leather boots and crafted in rich tones of taupe, herb and marsala, RelaxedLeather cases deliver smart, easy protection for the iPhone 7. Each genuine... Read more
Week’s Best Deal: New 2016 13-inch 2.0GHz Mac...
Amazon has the new 2016 13″ 2.0GHz non-Touch Bar MacBook Pros on sale for a limited time for $225 off MSRP including free shipping: - 13″ 2.0GHz MacBook Pro, Space Gray (MLL42LL/A): $1274.99 $225 off... Read more
Back in stock: Apple refurbished Mac minis fr...
Apple has Certified Refurbished Mac minis available starting at $419. Apple’s one-year warranty is included with each mini, and shipping is free: - 1.4GHz Mac mini: $419 $80 off MSRP - 2.6GHz Mac... Read more
Apple Ranked ‘Most Intimate Brand’
The top ranked ‘”intimate” brands continued to outperform the S&P and Fortune 500 indices in revenue and profit over the past 10 years, according to MBLM’s Brand Intimacy 2017 Report, the largest... Read more
B-Eng introduces SSD Health Check for Mac OS
Fehraltorf, Switzerland based independant Swiss company- B-Eng has announced the release and immediate availability of SSD Health Check 1.0, the company’s new hard drive utility for Mac OS X. As the... Read more
Apple’s Education discount saves up to $300 o...
Purchase a new Mac or iPad using Apple’s Education Store and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free: -... Read more
4-core 3.7GHz Mac Pro on sale for $2290, save...
Guitar Center has the 3.7GHz 4-core Mac Pro (MD253LL/A) on sale for $2289.97 including free shipping or free local store pickup (if available). Their price is a $710 savings over standard MSRP for... Read more
128GB Apple iPad Air 2, refurbished, availabl...
Apple has Certified Refurbished 128GB iPad Air 2s WiFis available for $419 including free shipping. That’s an $80 savings over standard MSRP for this model. A standard Apple one-year warranty is... Read more
13-inch 2.7GHz Retina MacBook Pro on sale for...
B&H Photo has the 2015 13″ 2.7GHz/128GB Retina Apple MacBook Pro on sale for $100 off MSRP. Shipping is free, and B&H charges NY tax only: - 13″ 2.7GHz/128GB Retina MacBook Pro (MF839LL/A): $... Read more
Laptop Market – Flight To Quality? – The ‘Boo...
Preliminary quarterly PC shipments data released by Gartner Inc. last week reveal an interesting disparity between sales performance of major name PC vendors as opposed to that of less well-known... Read more

Jobs Board

*Apple* macOS Systems Integration Administra...
…most exceptional support available in the industry. SCI is seeking an Junior Apple macOS systems integration administrator that will be responsible for providing Read more
*Apple* Retail - Multiple Positions- Deer Pa...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Technician - nfrastructure (United S...
Let’s Work Together Apple Technician This position is based in Portland, ME Life at nfrastructure At nfrastructure, we understand that our success results from our Read more
*Apple* Mobile Master - Best Buy (United Sta...
**467692BR** **Job Title:** Apple Mobile Master **Location Number:** 000602-Columbia MO-Store **Job Description:** **What does a Best Buy Apple Mobile Master Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.