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 (Magreeable@aol.com). 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.

 
AAPL
$102.13
Apple Inc.
+1.24
MSFT
$44.87
Microsoft Corpora
-0.14
GOOG
$571.00
Google Inc.
-6.86

MacTech Search:
Community Search:

Software Updates via MacUpdate

Viber 4.2.2 - Send messages and make cal...
Viber lets you send free messages and make free calls to other Viber users, on any device and network, in any country! Viber syncs your contacts, messages and call history with your mobile device,... Read more
Cocktail 7.6 - General maintenance and o...
Cocktail is a general purpose utility for OS X that lets you clean, repair and optimize your Mac. It is a powerful digital toolset that helps hundreds of thousands of Mac users around the world get... Read more
LaunchBar 6.1 - Powerful file/URL/email...
LaunchBar is an award-winning productivity utility that offers an amazingly intuitive and efficient way to search and access any kind of information stored on your computer or on the Web. It provides... Read more
Maya 2015 - Professional 3D modeling and...
Maya is an award-winning software and powerful, integrated 3D modeling, animation, visual effects, and rendering solution. Because Maya is based on an open architecture, all your work can be scripted... Read more
BBEdit 10.5.12 - Powerful text and HTML...
BBEdit is the leading professional HTML and text editor for the Mac. Specifically crafted in response to the needs of Web authors and software developers, this award-winning product provides a... Read more
Microsoft Office 2011 14.4.4 - Popular p...
Microsoft Office 2011 helps you create professional documents and presentations. And since Office for Mac 2011 is compatible with Office for Windows, you can work on documents with virtually anyone... Read more
TextWrangler 4.5.10 - Free general purpo...
TextWrangler is the powerful general purpose text editor, and Unix and server administrator's tool. Oh, and also, like the best things in life, it's free. TextWrangler is the "little brother" to... Read more
BitTorrent Sync 1.4.72 - Sync files secu...
BitTorrent Sync allows you to sync unlimited files between your own devices, or share a folder with friends and family to automatically sync anything. File transfers are encrypted. Your information... Read more
Cyberduck 4.5.2 - FTP and SFTP browser....
Cyberduck is a robust FTP/FTP-TLS/SFTP browser for the Mac whose lack of visual clutter and cleverly intuitive features make it easy to use. Support for external editors and system technologies such... Read more
Tinderbox 6.0.3 - Store and organize you...
Tinderbox is a personal content management assistant. It stores your notes, ideas, and plans. It can help you organize and understand them. And Tinderbox helps you share ideas through Web journals... Read more

Latest Forum Discussions

See All

ALONE... (Games)
ALONE... 1.0.1 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0.1 (iTunes) Description: ALONE is a handcrafted, intense survival journey through space. Navigate caves, rip through rocky debris, dodge rocks and comets... | Read more »
Almightree: The Last Dreamer (Games)
Almightree: The Last Dreamer 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: The world is shattering and you are the only hope to restore the balance. A thrilling and challenging 3D puzzle... | Read more »
The Nightmare Cooperative (Games)
The Nightmare Cooperative 1.1 Device: iOS Universal Category: Games Price: $3.99, Version: 1.1 (iTunes) Description: Fiendishly difficult! Adorably cute! Utterly engrossing!How much gold can you get before your entire team is... | Read more »
Mobile Convolution (Music)
Mobile Convolution 1.0.0 Device: iOS Universal Category: Music Price: $9.99, Version: 1.0.0 (iTunes) Description: | Read more »
Invaders! From Outer Space Review
Invaders! From Outer Space Review By Rob Thomas on August 27th, 2014 Our Rating: :: RETRO NOSTALGIAUniversal App - Designed for iPhone and iPad It’s a shame that Invaders! doesn’t offer deeper gameplay, as this retro-inspired... | Read more »
Spooklands Review
Spooklands Review By Jennifer Allen on August 27th, 2014 Our Rating: :: ONE-TOUCH SHOOTERUniversal App - Designed for iPhone and iPad One-touch simultaneously controls your direction and your weapon in this unique arena shooter.   | Read more »
Heroes of Order & Chaos Add Twitch I...
Heroes of Order & Chaos Add Twitch Integration, New Heroes, and More Posted by Ellis Spice on August 27th, 2014 [ permalink ] | Read more »
Foodie Yama Review
Foodie Yama Review By Jennifer Allen on August 27th, 2014 Our Rating: :: BRIEFLY HOOKSUniversal App - Designed for iPhone and iPad Foodie Yama will draw you in for a brief while, and you’ll never be entirely sure why.   | Read more »
Spotify Connect Turns One, Now Supports...
Spotify Connect Turns One, Now Supports New Devices Posted by Ellis Spice on August 27th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
The Rise of PicsaStock and How You Can M...
We all take plenty of photos, right? That’s the joy of having a reasonably powerful camera in your pocket, thanks to your trusty iPhone and a bevy of similarly useful apps. Wouldn’t it be great to make some money out of those snaps? While your... | Read more »

Price Scanner via MacPrices.net

12-Inch MacBook Air Coming in 4Q14 or 2015 –...
Digitimes’ Aaron Lee and Joseph Tsai report that according to Taiwan-based upstream supply chain insiders, Apple plans to launch a thinner MacBook model either at year end 2014 or in 2015, and that... Read more
Sapphire Screen “Most Wanted” iPhone 6 New Fe...
According to the ‘uSell.com iPhone Most Wanted Survey’ — a representative survey of 1,000 U.S. smartphone users conducted by used iPhone marketplace uSell.com — close to half of all smartphone users... Read more
The iPad’s Real Competitive Challenger (Not S...
It’s been my contention for some time that the iPad is suffering from something of an identity crisis, and I suspect that may be a factor in slackening sales this year. Apple can’t seem to decide... Read more
13-inch 2.6GHz/256GB Retina MacBook Pro on sa...
B&H Photo has the 13″ 2.6GHz/256GB Retina MacBook Pro on sale for $1379 including free shipping plus NY sales tax only. Their price is $120 off MSRP. Read more
Life Inventory iOS Apps – Learn to Know Thyse...
James Hollender’s Life Inventory apps s are now on sale with 20% off thru Labor Day, 09/01/2014. This is a great opportunity to get started on that Moral Inventory you’ve been putting off doing for... Read more
Pocket Watch, LLC. Reveals Cloud Server For P...
Beaumont, Texas based Pocket Watch, LLC. has announced the availability of its new ActivePrint Cloud Server Powered by Raspberry Pi. With this small standalone box almost any USB printer or available... Read more
902it Simplifies Area Code Changes For Nova S...
The east coast Canadian provinces of Nova Scotia and Prince Edward Island are phasing in 10 digit telephone dialing, to be fully in place by November, in order to accommodate a second area code to... Read more
Boomerang iPad Stand Mounts Your iPad Anywher...
Boomerang, a Mountable Stand with Multiple Viewing Angles, is now available for iPad Air. Boomerang combines several functions that aim to expand your iPad’s potential in one, elegant product. The... Read more
Retina MacBook Pros available starting at $10...
The Apple Store has Apple Certified Refurbished 13″ and 15″ MacBook Pros available starting at $929. Apple’s one-year warranty is standard, and shipping is free: - 13″ 2.5GHz MacBook Pros (4GB RAM/... Read more
Apple 27-inch Thunderbolt Display (refurbishe...
The Apple Store has Apple Certified Refurbished 27″ Thunderbolt Displays available for $799 including free shipping. That’s $200 off the cost of new models. Read more

Jobs Board

*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
Senior Event Manager, *Apple* Retail Market...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global event strategy. Delivering an overarching brand story; in-store, Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Project Manager / Business Analyst, WW *Appl...
…a senior project manager / business analyst to work within our Worldwide Apple Fulfillment Operations and the Business Process Re-engineering team. This role will work Read more
Position Opening at *Apple* - Apple (United...
…customers purchase our products, you're the one who helps them get more out of their new Apple technology. Your day in the Apple Store is filled with a range of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.